[
  {
    "path": ".air.toml",
    "content": "root = \".\"\ntestdata_dir = \"testdata\"\ntmp_dir = \"tmp\"\n\n[build]\nargs_bin = [\"server\"]\nbin = \"./tmp/main\"\ncmd = \"go build -o ./tmp/main .\"\ndelay = 0\nexclude_dir = [\"assets\", \"tmp\", \"vendor\", \"testdata\"]\nexclude_file = []\nexclude_regex = [\"_test.go\"]\nexclude_unchanged = false\nfollow_symlink = false\nfull_bin = \"\"\ninclude_dir = []\ninclude_ext = [\"go\", \"tpl\", \"tmpl\", \"html\"]\ninclude_file = []\nkill_delay = \"0s\"\nlog = \"build-errors.log\"\npoll = false\npoll_interval = 0\nrerun = false\nrerun_delay = 500\nsend_interrupt = false\nstop_on_error = false\n\n[color]\napp = \"\"\nbuild = \"yellow\"\nmain = \"magenta\"\nrunner = \"green\"\nwatcher = \"cyan\"\n\n[log]\nmain_only = false\ntime = false\n\n[misc]\nclean_on_exit = false\n\n[screen]\nclear_on_rebuild = false\nkeep_scroll = true\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: []\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/00-bug_report_zh.yml",
    "content": "name: \"错误报告\"\ndescription: 错误报告 / 问题\ntitle: \"[BUG] 请修改标题为您遇到的问题\"\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        感谢您花时间填写此错误报告。\n        请**务必**确认您的问题无重复，且不是因为您的操作、网络或第三方软件问题。\n\n  - type: checkboxes\n    attributes:\n      label: 请确认以下事项\n      description: |\n        您必须阅读、检查、确认、同意以下内容，否则您的问题一定会被直接关闭。\n        或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。\n      options:\n        - label: |\n            我已确认阅读并同意 [AGPL-3.0 第15条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) 。\n            本程序不提供任何明示或暗示的担保，使用风险由您自行承担。\n        - label: |\n            我已确认阅读并同意 [AGPL-3.0 第16条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) 。\n            无论何种情况，版权持有人或其他分发者均不对使用本程序所造成的任何损失承担责任。\n        - label: |\n            我确认我的描述清晰，语法礼貌，能帮助开发者快速定位问题，并符合社区规则。\n        - label: |\n            我已确认阅读了[OpenList文档](https://doc.oplist.org)。\n        - label: |\n            我已确认没有重复的问题或讨论。\n        - label: |\n            我已确认是`OpenList`的问题，而不是其他原因（例如 [网络](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1) ，`依赖`或`操作`）。\n        - label: |\n            我认为此问题必须由`OpenList`处理，而非第三方。\n        - label: |\n            我已确认这个问题在最新版本中没有被修复。\n        - label: |\n            我没有阅读这个清单，只是闭眼选中了所有的复选框，请关闭这个 Issue 。\n  - type: input\n    id: version\n    attributes:\n      label: OpenList 版本（必填）\n      description: |\n        您使用的是哪个版本的软件？请不要使用`latest`或`master`作为答案。\n      placeholder: v4.xx.xx\n    validations:\n      required: true\n  - type: input\n    id: driver\n    attributes:\n      label: 使用的存储驱动（必填）\n      description: |\n        您使用的是哪个存储驱动？\n      placeholder: \"例如: OneDrive\"\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: 问题描述（必填）\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: 日志（必填）\n      description: |\n        请复制粘贴错误日志，或者截图。(可隐藏隐私字段) [查看方法](https://doc.oplist.org/faq/howto#%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8Dbug)\n    validations:\n      required: true\n  - type: textarea\n    id: config\n    attributes:\n      label: 配置文件内容（必填）\n      description: |\n        请提供您的`OpenList`应用的配置文件，并截图相关存储配置。(可隐藏隐私字段)\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: 复现链接（可选）\n      description: |\n        请提供能复现此问题的链接。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01-bug_report_en.yml",
    "content": "name: \"Bug Report\"\ndescription: Bug Report / Issue\ntitle: \"[BUG] Please modify the title to describe the issue you are facing\"\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for taking the time to fill out this bug report.\n        Please **make sure** your issue is not a duplicate and is not caused by your own operation, network, or third-party software.\n\n  - type: checkboxes\n    attributes:\n      label: Please confirm the following\n      description: |\n        You must read, check, confirm, and agree to all the following, otherwise your issue will definitely be closed directly.\n        Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).\n      options:\n        - label: |\n            I have read and agree to [AGPL-3.0 Section 15](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) .\n            The program is provided \"as is\" without any warranties; you bear all risks of using it.\n        - label: |\n            I have read and agree to [AGPL-3.0 Section 16](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) .\n            The copyright holders and distributors are not liable for any damages resulting from the use or inability to use the program.\n        - label: |\n            I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.\n        - label: |\n            I have read the [OpenList documentation](https://doc.oplist.org).\n        - label: |\n            I confirm there are no duplicate issues or discussions.\n        - label: |\n            I confirm this is an `OpenList` issue, not caused by other reasons (such as [network](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1), dependencies, or operation).\n        - label: |\n            I believe this issue must be handled by `OpenList` and not by a third party.\n        - label: |\n            I confirm this issue is not fixed in the latest version.\n        - label: |\n            I have not read these checkboxes and therefore I just ticked them all, Please close this issue.\n  - type: input\n    id: version\n    attributes:\n      label: OpenList Version (required)\n      description: |\n        What version of the software are you using? Please do not use `latest` or `master` as the answer.\n      placeholder: v4.xx.xx\n    validations:\n      required: true\n  - type: input\n    id: driver\n    attributes:\n      label: Storage Driver Used (required)\n      description: |\n        Which storage driver are you using?\n      placeholder: \"e.g. OneDrive\"\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug Description (required)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs (required)\n      description: |\n        Please copy and paste any relevant log output or screenshots. (You may mask sensitive fields) [Guide](https://doc.oplist.org/faq/howto#how-to-quickly-locate-bugs)\n    validations:\n      required: true\n  - type: textarea\n    id: config\n    attributes:\n      label: Configuration File Content (required)\n      description: |\n        Please provide your `OpenList` application's configuration file and a screenshot of the relevant storage configuration. (You may mask sensitive fields)\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Reproduction Link (optional)\n      description: |\n        Please provide a link to a repo or page that can reproduce this issue.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-feature_request_zh.yml",
    "content": "name: \"功能请求\"\ndescription: 功能请求 / 增强\ntitle: \"[Feature] 请修改标题为您的功能名称\"\nlabels: [enhancement]\nbody:\n  - type: checkboxes\n    attributes:\n      label: 请确认以下事项\n      description: |\n        您必须阅读、检查、确认、同意以下内容，否则您的问题可能会被直接关闭。\n        或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。\n      options:\n        - label: |\n            我已确认阅读并同意 [AGPL-3.0 第15条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) 。\n            本程序不提供任何明示或暗示的担保，使用风险由您自行承担。\n        - label: |\n            我已确认阅读并同意 [AGPL-3.0 第16条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) 。\n            无论何种情况，版权持有人或其他分发者均不对使用本程序所造成的任何损失承担责任。\n        - label: |\n            我确认我的描述清晰，语法礼貌，能帮助开发者快速定位问题，并符合社区规则。\n        - label: |\n            我已确认阅读了[OpenList文档](https://doc.oplist.org)。\n        - label: |\n            我已确认没有重复的问题或讨论。\n        - label: |\n            我认为此问题必须由`OpenList`处理，而非第三方。\n        - label: |\n            我已确认此功能尚未被实现。\n        - label: |\n            我已确认此功能是合理的，且有普遍需求，并非我个人需要。\n        - label: |\n            我没有阅读这个清单，只是闭眼选中了所有的复选框，请关闭这个 Issue 。\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: 需求描述\n    validations:\n      required: true\n  - type: textarea\n    id: suggested-solution\n    attributes:\n      label: 实现思路\n      description: |\n        实现此需求的解决思路。\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: 附加信息\n      description: |\n        相关的任何其他上下文或截图，或者你觉得有帮助的信息\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/03-feature_request_en.yml",
    "content": "name: \"Feature Request\"\ndescription: Feature Request / Enhancement\ntitle: \"[Feature] Please modify the title to your feature name\"\nlabels: [enhancement]\nbody:\n  - type: checkboxes\n    attributes:\n      label: Please confirm the following\n      description: |\n        You must read, check, confirm, and agree to all the following, otherwise your request may be closed directly.\n        Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).\n      options:\n        - label: |\n            I have read and agree to [AGPL-3.0 Section 15](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.).\n            The program is provided \"as is\" without any warranties; you bear all risks of using it.\n        - label: |\n            I have read and agree to [AGPL-3.0 Section 16](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.).\n            The copyright holders and distributors are not liable for any damages resulting from the use or inability to use the program.\n        - label: |\n            I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.\n        - label: |\n            I have read the [OpenList documentation](https://doc.oplist.org).\n        - label: |\n            I confirm there are no duplicate issues or discussions.\n        - label: |\n            I believe this issue must be handled by `OpenList` and not by a third party.\n        - label: |\n            I confirm this feature has not been implemented yet.\n        - label: |\n            I confirm this feature is reasonable and has general demand, not just my personal need.\n        - label: |\n            I have not read these checkboxes and therefore I just ticked them all, Please close this issue.\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Feature Description\n    validations:\n      required: true\n  - type: textarea\n    id: suggested-solution\n    attributes:\n      label: Suggested Solution\n      description: |\n        Solution or approach to achieve this feature.\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Information\n      description: |\n        Any other context or screenshots related to this feature request, or information you find helpful.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: 问题和讨论\n    url: https://github.com/OpenListTeam/OpenList/discussions\n    about: 讨论、问题、想法等\n  - name: Questions & Discussions\n    url: https://github.com/OpenListTeam/OpenList/discussions\n    about: Discuss issues, ideas, etc.\n  - name: 即时聊天\n    url: https://t.me/OpenListTeam\n    about: 与我们聊天\n  - name: Chat\n    url: https://t.me/OpenListTeam\n    about: Chat with us\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n  Provide a general summary of your changes in the Title above.\n  The PR title must start with `feat(): `, `docs(): `, `fix(): `, `style(): `, or `refactor(): `, `chore(): `. For example: `feat(component): add new feature`.\n  If it spans multiple components, use the main component as the prefix and enumerate in the title, describe in the body.\n  For breaking changes, add `!` after the type, e.g., `feat(component)!: breaking change`.\n-->\n<!--\n  在上方标题中提供您更改的总体摘要。\n  PR 标题需以 `feat(): `, `docs(): `, `fix(): `, `style(): `, `refactor(): `, `chore(): ` 其中之一开头，例如：`feat(component): 新增功能`。\n  如果跨多个组件，请使用主要组件作为前缀，并在标题中枚举、描述中说明。\n  如果是破坏性变更，请在类型后添加 `!`，例如 `feat(component)!: 破坏性变更`。\n-->\n\n## Description / 描述\n\n<!-- Describe your changes in detail -->\n<!-- 详细描述您的更改 -->\n\n## Motivation and Context / 背景\n\n<!-- Why is this change required? What problem does it solve? -->\n<!-- 为什么需要此更改？它解决了什么问题？ -->\n\n<!-- If it fixes an open issue, please link to the issue here. -->\n<!-- 如果修复了一个打开的issue，请在此处链接到该issue -->\n\nCloses #XXXX\n\n<!-- or -->\n<!-- 或者 -->\n\nRelates to #XXXX\n\n## How Has This Been Tested? / 测试\n\n<!-- Please describe in detail how you tested your changes. -->\n<!-- 请详细描述您如何测试更改 -->\n\n## Checklist / 检查清单\n\n<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->\n<!-- 检查以下所有要点，并在所有适用的框中打`x` -->\n\n<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->\n<!-- 如果您对其中任何一项不确定，请不要犹豫提问。我们会帮助您！ -->\n\n- [ ] I have read the [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md) document.\n      我已阅读 [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md) 文档。\n- [ ] I have formatted my code with `go fmt` or [prettier](https://prettier.io/).\n      我已使用 `go fmt` 或 [prettier](https://prettier.io/) 格式化提交的代码。\n- [ ] I have added appropriate labels to this PR (or mentioned needed labels in the description if lacking permissions).\n      我已为此 PR 添加了适当的标签（如无权限或需要的标签不存在，请在描述中说明，管理员将后续处理）。\n- [ ] I have requested review from relevant code authors using the \"Request review\" feature when applicable.\n      我已在适当情况下使用\"Request review\"功能请求相关代码作者进行审查。\n- [ ] I have updated the repository accordingly (If it’s needed).\n      我已相应更新了相关仓库（若适用）。\n  - [ ] [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend) #XXXX\n  - [ ] [OpenList-Docs](https://github.com/OpenListTeam/OpenList-Docs) #XXXX\n"
  },
  {
    "path": ".github/workflows/beta_release.yml",
    "content": "name: Beta Release builds\n\non:\n  push:\n    branches: [\"main\"]\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: write\n\njobs:\n  changelog:\n    name: Beta Release Changelog\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Create or update ref\n        id: create-or-update-ref\n        uses: ovsds/create-or-update-ref-action@v1\n        with:\n          ref: tags/beta\n          sha: ${{ github.sha }}\n\n      - name: Delete beta tag\n        run: git tag -d beta\n        continue-on-error: true\n\n      - name: changelog # or changelogithub@0.12 if ensure the stable result\n        id: changelog\n        run: |\n          git tag -l\n          npx changelogithub --output CHANGELOG.md\n\n      - name: Upload assets to beta release\n        uses: softprops/action-gh-release@v2\n        with:\n          body_path: CHANGELOG.md\n          files: CHANGELOG.md\n          prerelease: true\n          tag_name: beta\n\n      - name: Upload assets to github artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: beta changelog\n          path: ${{ github.workspace }}/CHANGELOG.md\n          compression-level: 0\n          if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn`\n\n  release:\n    needs:\n      - changelog\n    strategy:\n      matrix:\n        include:\n          - target: \"!(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)\" # xgo and loongarch\n            hash: \"md5\"\n          - target: \"linux-!(arm*)-musl*\" #musl-not-arm\n            hash: \"md5-linux-musl\"\n          - target: \"linux-arm*-musl*\" #musl-arm\n            hash: \"md5-linux-musl-arm\"\n          - target: \"windows-arm64\" #win-arm64\n            hash: \"md5-windows-arm64\"\n          - target: \"windows7-*\" #win7\n            hash: \"md5-windows7\"\n          - target: \"android-*\" #android\n            hash: \"md5-android\"\n          - target: \"freebsd-*\" #freebsd\n            hash: \"md5-freebsd\"\n\n    name: Beta Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.25.0\"\n\n      - name: Setup web\n        run: bash build.sh dev web\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Build\n        uses: OpenListTeam/cgo-actions@v1.2.2\n        with:\n          targets: ${{ matrix.target }}\n          musl-target-format: $os-$musl-$arch\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          out-dir: build\n          output: openlist-$target$ext\n          musl-base-url: \"https://github.com/OpenListTeam/musl-compilers/releases/latest/download/\"\n          x-flags: |\n            github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at\n            github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>\n            github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit\n            github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag\n            github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling\n\n      - name: Compress\n        run: |\n          bash build.sh zip ${{ matrix.hash }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # See above\n      - name: Upload assets to beta release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n          prerelease: true\n          tag_name: beta\n\n      - name: Clean illegal characters from matrix.target\n        id: clean_target_name\n        run: |\n          ILLEGAL_CHARS_REGEX='[\":<>|*?\\\\/\\r\\n]'\n          CLEANED_TARGET=$(echo \"${{ matrix.target }}\" | sed -E \"s/$ILLEGAL_CHARS_REGEX//g\")\n          echo \"Original target: ${{ matrix.target }}\"\n          echo \"Cleaned target: $CLEANED_TARGET\"\n          echo \"cleaned_target=$CLEANED_TARGET\" >> $GITHUB_ENV\n\n      - name: Upload assets to github artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: beta builds for ${{ env.cleaned_target }}\n          path: ${{ github.workspace }}/build/compress/*\n          compression-level: 0\n          if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn`\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Test Build\n\non:\n  pull_request:\n    branches: [\"main\"]\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    strategy:\n      matrix:\n        target:\n          - darwin-amd64\n          - darwin-arm64\n          - windows-amd64\n          - linux-arm64-musl\n          - linux-amd64-musl\n          - windows-arm64\n          - android-arm64\n    name: Build ${{ matrix.target }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: benjlevesque/short-sha@v3.0\n        id: short-sha\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.25.0\"\n\n      - name: Setup web\n        run: bash build.sh dev web\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Build\n        uses: OpenListTeam/cgo-actions@v1.2.2\n        with:\n          targets: ${{ matrix.target }}\n          musl-target-format: $os-$musl-$arch\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          out-dir: build\n          x-flags: |\n            github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at\n            github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>\n            github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit\n            github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag\n            github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling\n          output: openlist$ext\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: openlist_${{ steps.short-sha.outputs.sha }}_${{ matrix.target }}\n          path: build/*\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Release Automatic changelog\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  contents: write\n\njobs:\n  changelog:\n    name: Create Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Delete beta tag\n        run: git tag -d beta\n        continue-on-error: true\n\n      - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result\n        env:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/issue_pr_comment.yml",
    "content": "name: Issue or PR Auto Reply\n\non:\n  issues:\n    types: [opened]\n  pull_request:\n    types: [opened]\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  auto-reply:\n    runs-on: ubuntu-latest\n    if: github.event_name == 'issues'\n    steps:\n      - name: Check issue for unchecked tasks and reply\n        uses: actions/github-script@v7\n        with:\n          script: |\n            let comment = \"\";\n            const issueTitle = context.payload.issue.title || \"\";\n            const titleNotEdited = /(请修改标题|Please modify the title)/i.test(issueTitle);\n            if (titleNotEdited) {\n              comment = \"⚠️ 请修改标题以更好地描述您的问题或需求，并删除示例提示。当前 Issue 将被自动关闭。如需继续提交，请创建新的 Issue。\\n\";\n              comment += \"⚠️ Please modify the title to better describe your issue or request, and remove the example prompt. This issue will be automatically closed. If you wish to proceed, please create a new issue.\\n\";\n              await github.rest.issues.createComment({\n                ...context.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n              await github.rest.issues.update({\n                ...context.repo,\n                issue_number: context.issue.number,\n                state: 'closed',\n                state_reason: 'not_planned',\n                labels: ['invalid']\n              });\n              return;\n            }\n            const issueBody = context.payload.issue.body || \"\";\n            const confirmHasRead = /- \\[ \\] (?!我没有阅读这个清单|I have not read these checkboxes)/.test(issueBody);\n            const confirmNotRead = /- \\[[xX]\\] (?:我没有阅读这个清单|I have not read these checkboxes)/.test(issueBody);\n            if (confirmNotRead) {\n              comment = \"⚠️ 你的 Issue 不符合提交规则。请先阅读相关规范后再重新提交。当前 Issue 将被自动关闭。如需继续提交，请确认已了解规则后重新打开或创建新的 Issue。\\n\";\n              comment += \"⚠️ Your issue does not comply with the submission rules. Please read the guidelines before submitting again. This issue will be automatically closed. If you wish to proceed, please confirm that you have reviewed the rules before reopening or creating a new issue.\\n\";\n              await github.rest.issues.createComment({\n                ...context.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n              await github.rest.issues.update({\n                ...context.repo,\n                issue_number: context.issue.number,\n                state: 'closed',\n                state_reason: 'not_planned',\n                labels: ['invalid']\n              });\n              return;\n            }\n            if (confirmHasRead) {\n              comment = \"感谢您联系OpenList。我们会尽快回复您。\\n\";\n              comment += \"Thanks for contacting OpenList. We will reply to you as soon as possible.\\n\\n\";\n              comment += \"由于您提出的 Issue 中包含部分未确认的项目，为了更好地管理项目，在人工审核后可能会直接关闭此问题。\\n\";\n              comment += \"如果您能确认并补充相关未确认项目的信息，欢迎随时重新提交。我们会及时关注并处理。感谢您的理解与支持！\\n\";\n              comment += \"Since your issue contains some unchecked tasks, it may be closed after manual review.\\n\";\n              comment += \"If you can confirm and provide information for the unchecked tasks, feel free to resubmit.\\n\";\n              comment += \"We will pay attention and handle it in a timely manner.\\n\\n\";\n              comment += \"感谢您的理解与支持！\\n\";\n              comment += \"Thank you for your understanding and support!\\n\";\n              await github.rest.issues.createComment({\n                ...context.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n            }\n\n  pr-title-check:\n    runs-on: ubuntu-latest\n    if: github.event_name == 'pull_request'\n    steps:\n      - name: Check PR title for required prefix and comment\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const title = context.payload.pull_request.title || \"\";\n            const ok = /^(feat|docs|fix|style|refactor|chore)\\(.+?\\)!?: /i.test(title);\n            if (!ok) {\n              let comment = \"⚠️ PR 标题需以 `feat(): `, `docs(): `, `fix(): `, `style(): `, `refactor(): `, `chore(): ` 其中之一开头，例如：`feat(component): 新增功能`。\\n\";\n              comment += \"⚠️ The PR title must start with `feat(): `, `docs(): `, `fix(): `, `style(): `, or `refactor(): `, `chore(): `. For example: `feat(component): add new feature`.\\n\\n\";\n              comment += \"如果跨多个组件，请使用主要组件作为前缀，并在标题中枚举、描述中说明。\\n\";\n              comment += \"If it spans multiple components, use the main component as the prefix and enumerate in the title, describe in the body.\\n\\n\";\n              comment += \"如果是破坏性变更，请在类型后添加 `!`，例如 `feat(component)!: 破坏性变更`。\\n\";\n              comment += \"For breaking changes, add `!` after the type, e.g., `feat(component)!: breaking change`.\\n\\n\";\n              await github.rest.issues.createComment({\n                ...context.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n            }\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release builds\n\non:\n  release:\n    types: [ published ]\n\npermissions:\n  contents: write\n\njobs:\n  # Set release to prerelease first\n  prerelease:\n    name: Set Prerelease\n    runs-on: ubuntu-latest\n    steps:\n      - name: Prerelease\n        uses: irongut/EditRelease@v1.2.0\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          id: ${{ github.event.release.id }}\n          prerelease: true\n\n  # Main release job for all platforms\n  release:\n    needs: prerelease\n    strategy:\n      matrix:\n        build-type: [ 'standard', 'lite' ]\n        target-platform: [ '', 'android', 'freebsd', 'linux_musl', 'linux_musl_arm' ]\n    name: Release ${{ matrix.target-platform && format('{0} ', matrix.target-platform) || '' }}${{ matrix.build-type == 'lite' && 'Lite' || '' }}\n    runs-on: ubuntu-latest\n    steps:\n\n      - name: Free Disk Space (Ubuntu)\n        if: matrix.target-platform == ''\n        uses: jlumbroso/free-disk-space@main\n        with:\n          tool-cache: false\n          android: true\n          dotnet: true\n          haskell: true\n          large-packages: true\n          docker-images: true\n          swap-storage: true\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.25.0'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install dependencies\n        if: matrix.target-platform == ''\n        run: |\n          sudo snap install zig --classic --beta\n          docker pull crazymax/xgo:latest\n          go install github.com/crazy-max/xgo@latest\n          sudo apt install upx\n\n      - name: Build\n        run: |\n          bash build.sh release ${{ matrix.build-type == 'lite' && 'lite' || '' }} ${{ matrix.target-platform }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n          prerelease: false\n          tag_name: ${{ github.event.release.tag_name }}\n\n"
  },
  {
    "path": ".github/workflows/release_docker.yml",
    "content": "name: Release builds (Docker)\n\non:\n  workflow_dispatch:\n    inputs:\n      manual_tag:\n        description: 'Tag name (like v0.1.0). Required if as_latest is true.'\n        required: false\n        type: string\n      as_latest:\n        description: 'Tag as latest?'\n        required: true\n        default: 'false'\n        type: choice\n        options:\n          - 'true'\n          - 'false'\n  push:\n    tags:\n      - 'v*'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  DOCKERHUB_ORG_NAME: ${{ vars.DOCKERHUB_ORG_NAME || 'openlistteam' }}\n  GHCR_ORG_NAME: ${{ vars.GHCR_ORG_NAME || 'openlistteam' }}\n  IMAGE_NAME: openlist-git\n  IMAGE_NAME_DOCKERHUB: openlist\n  REGISTRY: ghcr.io\n  ARTIFACT_NAME: 'binaries_docker_release'\n  ARTIFACT_NAME_LITE: 'binaries_docker_release_lite'\n  RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/ppc64le,linux/riscv64,linux/loong64' ### Temporarily disable Docker builds for linux/s390x architectures for unknown reasons.\n  IMAGE_PUSH: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}\n\npermissions:\n  packages: write\n\njobs:\n  build_binary:\n    name: Build Binaries for Docker Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '1.25.0'\n\n      - name: Cache Musl\n        id: cache-musl\n        uses: actions/cache@v4\n        with:\n          path: build/musl-libs\n          key: docker-musl-libs-v2\n\n      - name: Download Musl Library\n        if: steps.cache-musl.outputs.cache-hit != 'true'\n        run: bash build.sh prepare docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build go binary (release)\n        run: bash build.sh release docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          overwrite: true\n          path: |\n            build/\n            !build/*.tgz\n            !build/musl-libs/**\n\n  build_binary_lite:\n    name: Build Binaries for Docker Release (Lite)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '1.25.0'\n\n      - name: Cache Musl\n        id: cache-musl\n        uses: actions/cache@v4\n        with:\n          path: build/musl-libs\n          key: docker-musl-libs-v2\n\n      - name: Download Musl Library\n        if: steps.cache-musl.outputs.cache-hit != 'true'\n        run: bash build.sh prepare lite docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build go binary (release)\n        run: bash build.sh release lite docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME_LITE }}\n          overwrite: true\n          path: |\n            build/\n            !build/*.tgz\n            !build/musl-libs/**\n\n  release_docker:\n    needs: build_binary\n    name: Release Docker image\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        image: [\"latest\", \"ffmpeg\", \"aria2\", \"aio\"]\n        include:\n          - image: \"latest\"\n            base_image_tag: \"base\"\n            build_arg: \"\"\n            tag_favor: \"\"\n          - image: \"ffmpeg\"\n            base_image_tag: \"ffmpeg\"\n            build_arg: INSTALL_FFMPEG=true\n            tag_favor: \"suffix=-ffmpeg,onlatest=true\"\n          - image: \"aria2\"\n            base_image_tag: \"aria2\"\n            build_arg: INSTALL_ARIA2=true\n            tag_favor: \"suffix=-aria2,onlatest=true\"\n          - image: \"aio\"\n            base_image_tag: \"aio\"\n            build_arg: |\n              INSTALL_FFMPEG=true\n              INSTALL_ARIA2=true\n            tag_favor: \"suffix=-aio,onlatest=true\"\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - uses: actions/download-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          path: 'build/'\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to GitHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Login to DockerHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}\n            ${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}\n          tags: >\n            ${{ github.event_name == 'workflow_dispatch'\n                && format('type=raw,value={0}', github.event.inputs.manual_tag)\n              || format('type=raw,value={0}', github.ref_name) }}\n          flavor: |\n            latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }}\n            ${{ matrix.tag_favor }}\n\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: Dockerfile.ci\n          push: ${{ env.IMAGE_PUSH == 'true' }}\n          build-args: |\n            BASE_IMAGE_TAG=${{ matrix.base_image_tag }}\n            ${{ matrix.build_arg }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: ${{ env.RELEASE_PLATFORMS }}\n\n  release_docker_lite:\n    needs: build_binary_lite\n    name: Release Docker image (Lite)\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        image: [\"latest\", \"ffmpeg\", \"aria2\", \"aio\"]\n        include:\n          - image: \"latest\"\n            base_image_tag: \"base\"\n            build_arg: \"\"\n            tag_favor: \"suffix=-lite,onlatest=true\"\n          - image: \"ffmpeg\"\n            base_image_tag: \"ffmpeg\"\n            build_arg: INSTALL_FFMPEG=true\n            tag_favor: \"suffix=-lite-ffmpeg,onlatest=true\"\n          - image: \"aria2\"\n            base_image_tag: \"aria2\"\n            build_arg: INSTALL_ARIA2=true\n            tag_favor: \"suffix=-lite-aria2,onlatest=true\"\n          - image: \"aio\"\n            base_image_tag: \"aio\"\n            build_arg: |\n              INSTALL_FFMPEG=true\n              INSTALL_ARIA2=true\n            tag_favor: \"suffix=-lite-aio,onlatest=true\"\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - uses: actions/download-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME_LITE }}\n          path: 'build/'\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to GitHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Login to DockerHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}\n            ${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}\n          tags: >\n            ${{ github.event_name == 'workflow_dispatch'\n                && format('type=raw,value={0}', github.event.inputs.manual_tag)\n              || format('type=raw,value={0}', github.ref_name) }}\n          flavor: |\n            latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }}\n            ${{ matrix.tag_favor }}\n\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: Dockerfile.ci\n          push: ${{ env.IMAGE_PUSH == 'true' }}\n          build-args: |\n            BASE_IMAGE_TAG=${{ matrix.base_image_tag }}\n            ${{ matrix.build_arg }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: ${{ env.RELEASE_PLATFORMS }}\n"
  },
  {
    "path": ".github/workflows/sync_repo.yml",
    "content": "name: Sync to Gitee\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  sync:\n    runs-on: ubuntu-latest\n    name: Sync GitHub to Gitee\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup SSH\n        run: |\n          mkdir -p ~/.ssh\n          echo \"${{ secrets.GITEE_SSH_PRIVATE_KEY }}\" > ~/.ssh/id_rsa\n          chmod 600 ~/.ssh/id_rsa\n          ssh-keyscan gitee.com >> ~/.ssh/known_hosts\n\n      - name: Create single commit and push\n        run: |\n          git config user.name \"GitHub Actions\"\n          git config user.email \"actions@github.com\"\n          \n          # Create a new branch\n          git checkout --orphan new-main\n          git add .\n          git commit -m \"Sync from GitHub: $(date)\"\n\n          # Add Gitee remote and force push\n          git remote add gitee ${{ vars.GITEE_REPO_URL }}\n          git push --force gitee new-main:main\n"
  },
  {
    "path": ".github/workflows/test_docker.yml",
    "content": "name: Beta Release (Docker)\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  DOCKERHUB_ORG_NAME: ${{ vars.DOCKERHUB_ORG_NAME || 'openlistteam' }}\n  GHCR_ORG_NAME: ${{ vars.GHCR_ORG_NAME || 'openlistteam' }}\n  IMAGE_NAME: openlist-git\n  IMAGE_NAME_DOCKERHUB: openlist\n  REGISTRY: ghcr.io\n  ARTIFACT_NAME: 'binaries_docker_release'\n  RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/ppc64le,linux/riscv64,linux/loong64' ### Temporarily disable Docker builds for linux/s390x architectures for unknown reasons.\n  IMAGE_PUSH: ${{ github.event_name == 'push' }}\n  IMAGE_TAGS_BETA: |\n    type=ref,event=pr\n    type=raw,value=beta,enable={{is_default_branch}}\n\njobs:\n  build_binary:\n    name: Build Binaries for Docker Release (Beta)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '1.25.0'\n\n      - name: Cache Musl\n        id: cache-musl\n        uses: actions/cache@v4\n        with:\n          path: build/musl-libs\n          key: docker-musl-libs-v2\n\n      - name: Download Musl Library\n        if: steps.cache-musl.outputs.cache-hit != 'true'\n        run: bash build.sh prepare docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build go binary (beta)\n        run: bash build.sh beta docker-multiplatform\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          overwrite: true\n          path: |\n            build/\n            !build/*.tgz\n            !build/musl-libs/**\n\n  release_docker:\n    needs: build_binary\n    name: Release Docker image (Beta)\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n    strategy:\n      matrix:\n        image: [\"latest\", \"ffmpeg\", \"aria2\", \"aio\"]\n        include:\n          - image: \"latest\"\n            base_image_tag: \"base\"\n            build_arg: \"\"\n            tag_favor: \"\"\n          - image: \"ffmpeg\"\n            base_image_tag: \"ffmpeg\"\n            build_arg: INSTALL_FFMPEG=true\n            tag_favor: \"suffix=-ffmpeg,onlatest=true\"\n          - image: \"aria2\"\n            base_image_tag: \"aria2\"\n            build_arg: INSTALL_ARIA2=true\n            tag_favor: \"suffix=-aria2,onlatest=true\"\n          - image: \"aio\"\n            base_image_tag: \"aio\"\n            build_arg: |\n              INSTALL_FFMPEG=true\n              INSTALL_ARIA2=true\n            tag_favor: \"suffix=-aio,onlatest=true\"\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - uses: actions/download-artifact@v4\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          path: 'build/'\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to GitHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Login to DockerHub Container Registry\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}\n            ${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}\n          tags: ${{ env.IMAGE_TAGS_BETA }}\n          flavor: |\n            ${{ matrix.tag_favor }}\n\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: Dockerfile.ci\n          push: ${{ env.IMAGE_PUSH == 'true' }}\n          build-args: |\n            BASE_IMAGE_TAG=${{ matrix.base_image_tag }}\n            ${{ matrix.build_arg }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: ${{ env.RELEASE_PLATFORMS }}\n"
  },
  {
    "path": ".github/workflows/trigger-makefile-update.yml",
    "content": "name: Trigger OpenWRT Update\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Release tag to trigger update for'\n        required: true\n        type: string\n\njobs:\n  trigger-makefile-update:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger Makefile hash update\n        uses: peter-evans/repository-dispatch@v3\n        with:\n          token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}\n          repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}\n          event-type: update-hashes\n          client-payload: |\n            {\n              \"source_repository\": \"${{ github.repository }}\",\n              \"release_tag\": \"${{ inputs.tag || github.ref_name }}\",\n              \"release_name\": \"${{ inputs.tag || github.ref_name }}\",\n              \"release_url\": \"${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}\",\n              \"triggered_by\": \"${{ github.actor }}\",\n              \"trigger_reason\": \"${{ github.event_name }}\"\n            }\n\n      - name: Log trigger information\n        run: |\n          echo \"🚀 Successfully triggered Makefile hash update\"\n          echo \"📦 Target repository: OpenListTeam/luci-app-openlist\"\n          echo \"🏷️ Tag: ${{ inputs.tag || github.ref_name }}\"\n          echo \"👤 Triggered by: ${{ github.actor }}\"\n          echo \"📅 Trigger time: $(date -u '+%Y-%m-%d %H:%M:%S UTC')\""
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.DS_Store\noutput/\n/dist/\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n*.db\n*.bin\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n/bin/*\n*.json\n/build\n/data/\n/tmp/\n/log/\n/lang/\n/daemon/\n/public/dist/*\n/!public/dist/README.md\n\n.VSCodeCounter"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n[Telegram Group](https://t.me/OpenListTeam).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Setup your machine\n\n`OpenList` is written in [Go](https://golang.org/) and [SolidJS](https://www.solidjs.com/).\n\nPrerequisites:\n\n- [git](https://git-scm.com)\n- [Go 1.24+](https://golang.org/doc/install)\n- [gcc](https://gcc.gnu.org/)\n- [nodejs](https://nodejs.org/)\n\n## Cloning a fork\n\nFork and clone `OpenList` and `OpenList-Frontend` anywhere:\n\n```shell\n$ git clone https://github.com/<your-username>/OpenList.git\n$ git clone --recurse-submodules https://github.com/<your-username>/OpenList-Frontend.git\n```\n\n## Creating a branch\n\nCreate a new branch from the `main` branch, with an appropriate name.\n\n```shell\n$ git checkout -b <branch-name>\n```\n\n## Preview your change\n\n### backend\n\n```shell\n$ go run main.go\n```\n\n### frontend\n\n```shell\n$ pnpm dev\n```\n\n## Add a new driver\n\nCopy `drivers/template` folder and rename it, and follow the comments in it.\n\n## Create a commit\n\nCommit messages should be well formatted, and to make that \"standardized\".\n\nSubmit your pull request. For PR titles, follow [Conventional Commits](https://www.conventionalcommits.org).\n\nhttps://github.com/OpenListTeam/OpenList/issues/376\n\nIt's suggested to sign your commits. See: [How to sign commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)\n\n## Submit a pull request\n\nPlease make sure your code has been formatted with `go fmt` or [prettier](https://prettier.io/) before submitting.\n\nPush your branch to your `openlist` fork and open a pull request against the `main` branch.\n\n## Merge your pull request\n\nYour pull request will be merged after review. Please wait for the maintainer to merge your pull request after review.\n\nAt least 1 approving review is required by reviewers with write access. You can also request a review from maintainers.\n\n## Delete your branch\n\n(Optional) After your pull request is merged, you can delete your branch.\n\n---\n\nThank you for your contribution! Let's make OpenList better together!\n"
  },
  {
    "path": "Dockerfile",
    "content": "### Default image is base. You can add other support by modifying BASE_IMAGE_TAG. The following parameters are supported: base (default), aria2, ffmpeg, aio\nARG BASE_IMAGE_TAG=base\n\nFROM alpine:edge AS builder\nLABEL stage=go-builder\nWORKDIR /app/\nRUN apk add --no-cache bash curl jq gcc git go musl-dev\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY ./ ./\nRUN bash build.sh release docker\n\nFROM openlistteam/openlist-base-image:${BASE_IMAGE_TAG}\nLABEL MAINTAINER=\"OpenList\"\nARG INSTALL_FFMPEG=false\nARG INSTALL_ARIA2=false\nARG USER=openlist\nARG UID=1001\nARG GID=1001\n\nWORKDIR /opt/openlist/\n\nRUN addgroup -g ${GID} ${USER} && \\\n    adduser -D -u ${UID} -G ${USER} ${USER} && \\\n    mkdir -p /opt/openlist/data\n\nCOPY --from=builder --chmod=755 --chown=${UID}:${GID} /app/bin/openlist ./\nCOPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh\n\nUSER ${USER}\nRUN /entrypoint.sh version\n\nENV UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}\nVOLUME /opt/openlist/data/\nEXPOSE 5244 5245\nCMD [ \"/entrypoint.sh\" ]\n"
  },
  {
    "path": "Dockerfile.ci",
    "content": "ARG BASE_IMAGE_TAG=base\nFROM ghcr.io/openlistteam/openlist-base-image:${BASE_IMAGE_TAG}\nLABEL MAINTAINER=\"OpenList\"\nARG TARGETPLATFORM\nARG INSTALL_FFMPEG=false\nARG INSTALL_ARIA2=false\nARG USER=openlist\nARG UID=1001\nARG GID=1001\n\nWORKDIR /opt/openlist/\n\nRUN addgroup -g ${GID} ${USER} && \\\n    adduser -D -u ${UID} -G ${USER} ${USER} && \\\n    mkdir -p /opt/openlist/data\n\nCOPY --chmod=755 --chown=${UID}:${GID} /build/${TARGETPLATFORM}/openlist ./\nCOPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh\n\nUSER ${USER}\nRUN /entrypoint.sh version\n\nENV UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}\nVOLUME /opt/openlist/data/\nEXPOSE 5244 5245\nCMD [ \"/entrypoint.sh\" ]"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img style=\"width: 128px; height: 128px;\" src=\"https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg\" alt=\"logo\" />\n\n  <p><em>OpenList is a resilient, long-term governance, community-driven fork of AList — built to defend open source against trust-based attacks.</em></p>\n\n  <img src=\"https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3\" alt=\"latest version\" />\n  <a href=\"https://github.com/OpenListTeam/OpenList/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/OpenListTeam/OpenList\" alt=\"License\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild\"><img src=\"https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main\" alt=\"Build status\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/release/OpenListTeam/OpenList\" alt=\"latest version\" /></a>\n\n  <a href=\"https://github.com/OpenListTeam/OpenList/discussions\"><img src=\"https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936\" alt=\"discussions\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" /></a>\n</div>\n\n---\n\n- English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md)\n\n- [Contributing](./CONTRIBUTING.md)\n- [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)\n- [LICENSE](./LICENSE)\n\n## Disclaimer\n\nOpenList is an open-source project independently maintained by the OpenList Team, following the AGPL-3.0 license and committed to maintaining complete code openness and modification transparency.\n\nWe have noticed the emergence of some third-party projects in the community with names similar to this project, such as OpenListApp/OpenListApp, as well as some paid proprietary software using the same or similar naming. To avoid user confusion, we hereby declare:\n\n- OpenList has no official association with any third-party derivative projects.\n\n- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.\n\n- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments, and the use of existing features does not involve any costs.\n\nWe respect the community's rights to free use and derivative development, but we also strongly urge downstream projects:\n\n- Should not use the \"OpenList\" name for impersonation promotion or commercial gain;\n\n- Must not distribute OpenList-based code in a closed-source manner or violate AGPL license terms.\n\nTo better maintain healthy ecosystem development, we recommend:\n\n- Clearly indicate the project source and choose appropriate open-source licenses in accordance with the open-source spirit;\n\n- If involving commercial use, please avoid using \"OpenList\" or any confusing naming as the project name;\n\n- If you need to use materials located under OpenListTeam/Logo, you may modify and use them under compliance with the agreement.\n\nThank you for your support and understanding of the OpenList project.\n\n## Features\n\n- [x] Multiple storages\n  - [x] Local storage\n  - [x] [Aliyundrive](https://www.alipan.com)\n  - [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)\n  - [x] [189cloud](https://cloud.189.cn) (Personal, Family)\n  - [x] [GoogleDrive](https://drive.google.com)\n  - [x] [123pan](https://www.123pan.com)\n  - [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)\n  - [x] [PikPak](https://www.mypikpak.com)\n  - [x] [S3](https://aws.amazon.com/s3)\n  - [x] [Seafile](https://seafile.com)\n  - [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)\n  - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)\n  - [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))\n  - [x] [MediaFire](https://www.mediafire.com)\n  - [x] [Mediatrack](https://www.mediatrack.cn)\n  - [x] [ProtonDrive](https://proton.me/drive)\n  - [x] [139yun](https://yun.139.com) (Personal, Family, Group)\n  - [x] [YandexDisk](https://disk.yandex.com)\n  - [x] [BaiduNetdisk](http://pan.baidu.com)\n  - [x] [Terabox](https://www.terabox.com/main)\n  - [x] [UC](https://drive.uc.cn)\n  - [x] [Quark](https://pan.quark.cn)\n  - [x] [Thunder](https://pan.xunlei.com)\n  - [x] [Lanzou](https://www.lanzou.com)\n  - [x] [ILanzou](https://www.ilanzou.com)\n  - [x] [Google photo](https://photos.google.com)\n  - [x] [Mega.nz](https://mega.nz)\n  - [x] [Baidu photo](https://photo.baidu.com)\n  - [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)\n  - [x] [115](https://115.com)\n  - [X] [Cloudreve](https://cloudreve.org)\n  - [x] [Dropbox](https://www.dropbox.com)\n  - [x] [FeijiPan](https://www.feijipan.com)\n  - [x] [dogecloud](https://www.dogecloud.com/product/oss)\n  - [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)\n  - [x] [Chaoxing](https://www.chaoxing.com)\n  - [x] [CNB](https://cnb.cool/)\n  - [x] [Degoo](https://degoo.com)\n  - [x] [Doubao](https://www.doubao.com)\n  - [x] [Febbox](https://www.febbox.com)\n  - [x] [GitHub](https://github.com)\n  - [x] [OpenList](https://github.com/OpenListTeam/OpenList)\n  - [x] [Teldrive](https://github.com/tgdrive/teldrive)\n  - [x] [Weiyun](https://www.weiyun.com)\n- [x] Easy to deploy and out-of-the-box\n- [x] File preview (PDF, markdown, code, plain text, ...)\n- [x] Image preview in gallery mode\n- [x] Video and audio preview, support lyrics and subtitles\n- [x] Office documents preview (docx, pptx, xlsx, ...)\n- [x] `README.md` preview rendering\n- [x] File permalink copy and direct file download\n- [x] Dark mode\n- [x] I18n\n- [x] Protected routes (password protection and authentication)\n- [x] WebDAV\n- [x] Docker Deploy\n- [x] Cloudflare Workers proxy\n- [x] File/Folder package download\n- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy\n- [x] Offline download\n- [x] Copy files between two storage\n- [x] Multi-thread downloading acceleration for single-thread download/stream\n\n## Document\n\n- 📘 [Global Site](https://doc.oplist.org)\n- 📚 [Backup Site](https://doc.openlist.team)\n- 🌏 [CN Site](https://doc.oplist.org.cn)\n\n## Demo\n\n- 🌎 [Global Demo](https://demo.oplist.org)\n- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)\n\n## Discussion\n\nPlease refer to [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) for raising general questions, ***Issues* is for bug reports and feature requests only.**\n\n## Sponsor\n\n[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town \"VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.\")\n\n## License\n\nThe `OpenList` is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.\n\n## Disclaimer\n\n- This project is a free and open-source software designed to facilitate file sharing via net disks, primarily intended to support the downloading and learning of the Go programming language.\n- Please comply with all applicable laws and regulations when using this software. Any form of misuse is strictly prohibited.\n- The software is based on official SDKs or APIs without any modification, disruption, or interference with their behavior.\n- It only performs HTTP 302 redirects or traffic forwarding, and does not intercept, store, or tamper with any user data.\n- This project is not affiliated with any official platform or service provider.\n- The software is provided \"as is\", without any warranties of any kind, either express or implied, including but not limited to warranties of merchantability or fitness for a particular purpose.\n- The maintainers are not liable for any direct or indirect damages arising from the use of, or inability to use, this software.\n- You are solely responsible for any risks associated with using this software, including but not limited to account bans or download speed limitations.\n- This project is licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) License. Please see the [LICENSE](./LICENSE) file for details.\n\n## Contact Us\n\n- [@GitHub](https://github.com/OpenListTeam)\n- [Telegram Group](https://t.me/OpenListTeam)\n- [Telegram Channel](https://t.me/OpenListOfficial)\n\n## Contributors\n\nWe sincerely thank the author [Xhofe](https://github.com/Xhofe) of the original project [AlistGo/alist](https://github.com/AlistGo/alist) and all other contributors.\n\nThanks goes to these wonderful people:\n\n[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)\n"
  },
  {
    "path": "README_cn.md",
    "content": "<div align=\"center\">\n  <img style=\"width: 128px; height: 128px;\" src=\"https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg\" alt=\"logo\" />\n\n  <p><em>OpenList 是一个有韧性、长期治理、社区驱动的 AList 分支，旨在防御基于信任的开源攻击。</em></p>\n\n  <img src=\"https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3\" alt=\"latest version\" />\n  <a href=\"https://github.com/OpenListTeam/OpenList/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/OpenListTeam/OpenList\" alt=\"License\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild\"><img src=\"https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main\" alt=\"Build status\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/release/OpenListTeam/OpenList\" alt=\"latest version\" /></a>\n\n  <a href=\"https://github.com/OpenListTeam/OpenList/discussions\"><img src=\"https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936\" alt=\"discussions\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" /></a>\n</div>\n\n---\n\n- [English](./README.md) | 中文 | [日本語](./README_ja.md) | [Dutch](./README_nl.md)\n\n- [贡献指南](./CONTRIBUTING.md)\n- [行为准则](./CODE_OF_CONDUCT.md)\n- [许可证](./LICENSE)\n\n## 免责声明\n\nOpenList 是一个由 OpenList 团队独立维护的开源项目，遵循 AGPL-3.0 许可证，致力于保持完整的代码开放性和修改透明性。\n\n我们注意到社区中出现了一些与本项目名称相似的第三方项目，如 OpenListApp/OpenListApp，以及部分采用相同或近似命名的收费专有软件。为避免用户误解，现声明如下：\n\n- OpenList 与任何第三方衍生项目无官方关联。\n\n- 本项目的全部软件、代码与服务由 OpenList 团队维护，可在 GitHub 免费获取。\n\n- 项目文档与 API 服务均主要依托于 Cloudflare 提供的公益资源，目前无任何收费计划或商业部署，现有功能使用不涉及任何支出。\n\n我们尊重社区的自由使用与衍生开发权利，但也强烈呼吁下游项目：\n\n- 不应以“OpenList”名义进行冒名宣传或获取商业利益；\n\n- 不得将基于 OpenList 的代码进行闭源分发或违反 AGPL 许可证条款。\n\n为了更好地维护生态健康发展，我们建议：\n\n- 明确注明项目来源，并以符合开源精神的方式选择适当的开源许可证；\n\n- 如涉及商业用途，请避免使用“OpenList”或任何会产生混淆的方式作为项目名称；\n\n- 若需使用本项目位于 OpenListTeam/Logo 下的素材，可在遵守协议的前提下进行修改后使用。\n\n感谢您对 OpenList 项目的支持与理解。\n\n## 功能\n\n- [x] 多种存储\n  - [x] 本地存储\n  - [x] [阿里云盘](https://www.alipan.com)\n  - [x] OneDrive / Sharepoint ([国际版](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [中国](https://portal.partner.microsoftonline.cn), DE, US)\n  - [x] [天翼云盘](https://cloud.189.cn)（个人、家庭）\n  - [x] [GoogleDrive](https://drive.google.com)\n  - [x] [123云盘](https://www.123pan.com)\n  - [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)\n  - [x] [PikPak](https://www.mypikpak.com)\n  - [x] [S3](https://aws.amazon.com/s3)\n  - [x] [Seafile](https://seafile.com)\n  - [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)\n  - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)\n  - [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))\n  - [x] [MediaFire](https://www.mediafire.com)\n  - [x] [分秒帧](https://www.mediatrack.cn)\n  - [x] [ProtonDrive](https://proton.me/drive)\n  - [x] [和彩云](https://yun.139.com)（个人、家庭、群组）\n  - [x] [YandexDisk](https://disk.yandex.com)\n  - [x] [百度网盘](http://pan.baidu.com)\n  - [x] [Terabox](https://www.terabox.com/main)\n  - [x] [UC网盘](https://drive.uc.cn)\n  - [x] [夸克网盘](https://pan.quark.cn)\n  - [x] [迅雷网盘](https://pan.xunlei.com)\n  - [x] [蓝奏云](https://www.lanzou.com)\n  - [x] [蓝奏云优享版](https://www.ilanzou.com)\n  - [x] [Google 相册](https://photos.google.com)\n  - [x] [Mega.nz](https://mega.nz)\n  - [x] [百度相册](https://photo.baidu.com)\n  - [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)\n  - [x] [115](https://115.com)\n  - [x] [Cloudreve](https://cloudreve.org)\n  - [x] [Dropbox](https://www.dropbox.com)\n  - [x] [飞机盘](https://www.feijipan.com)\n  - [x] [多吉云](https://www.dogecloud.com/product/oss)\n  - [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)\n  - [x] [超星](https://www.chaoxing.com)\n  - [x] [CNB](https://cnb.cool/)\n  - [x] [Degoo](https://degoo.com)\n  - [x] [豆包](https://www.doubao.com)\n  - [x] [Febbox](https://www.febbox.com)\n  - [x] [GitHub](https://github.com)\n  - [x] [OpenList](https://github.com/OpenListTeam/OpenList)\n  - [x] [Teldrive](https://github.com/tgdrive/teldrive)\n  - [x] [微云](https://www.weiyun.com)\n- [x] 部署方便，开箱即用\n- [x] 文件预览（PDF、markdown、代码、纯文本等）\n- [x] 画廊模式下的图片预览\n- [x] 视频和音频预览，支持歌词和字幕\n- [x] Office 文档预览（docx、pptx、xlsx 等）\n- [x] `README.md` 预览渲染\n- [x] 文件永久链接复制和直接文件下载\n- [x] 黑暗模式\n- [x] 国际化\n- [x] 受保护的路由（密码保护和认证）\n- [x] WebDAV\n- [x] Docker 部署\n- [x] Cloudflare Workers 代理\n- [x] 文件/文件夹打包下载\n- [x] 网页上传（可允许访客上传）、删除、新建文件夹、重命名、移动和复制\n- [x] 离线下载\n- [x] 跨存储复制文件\n- [x] 单文件多线程下载/流式加速\n\n## 文档\n\n- 🌏 [国内站点](https://doc.oplist.org.cn)\n- 📘 [海外站点](https://doc.oplist.org)\n- 📚 [备用站点](https://doc.openlist.team)\n\n## 演示\n\n- 🇨🇳 [国内演示站](https://demo.oplist.org.cn)\n- 🌎 [海外演示站](https://demo.oplist.org)\n\n## 讨论\n\n如有一般性问题请前往 [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) 讨论区，***Issues* 仅用于错误报告和功能请求。**\n\n## 赞助者\n\n[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town \"VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.\")\n\n## 许可证\n\n`OpenList` 是基于 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证的开源软件。\n\n## 免责声明\n\n- 本项目为免费开源软件，旨在通过网盘便捷分享文件，主要用于 Go 语言的下载与学习。\n- 使用本软件时请遵守相关法律法规，严禁任何形式的滥用。\n- 本软件基于官方 SDK 或 API 实现，未对其行为进行任何修改、破坏或干扰。\n- 仅进行 HTTP 302 跳转或流量转发，不拦截、存储或篡改任何用户数据。\n- 本项目与任何官方平台或服务提供商无关。\n- 本软件按“原样”提供，不附带任何明示或暗示的担保，包括但不限于适销性或特定用途的适用性。\n- 维护者不对因使用或无法使用本软件而导致的任何直接或间接损失负责。\n- 您需自行承担使用本软件的所有风险，包括但不限于账号被封、下载限速等。\n- 本项目遵循 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证，详情请参见 [LICENSE](./LICENSE) 文件。\n\n## 联系我们\n\n- [@GitHub](https://github.com/OpenListTeam)\n- [Telegram 交流群](https://t.me/OpenListTeam)\n- [Telegram 频道](https://t.me/OpenListOfficial)\n\n## 贡献者\n\n我们衷心感谢原项目 [AlistGo/alist](https://github.com/AlistGo/alist) 的作者 [Xhofe](https://github.com/Xhofe) 及所有其他贡献者。\n\n感谢这些优秀的人：\n\n[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)\n"
  },
  {
    "path": "README_ja.md",
    "content": "<div align=\"center\">\n  <img style=\"width: 128px; height: 128px;\" src=\"https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg\" alt=\"logo\" />\n\n  <p><em>OpenList は、信頼ベースの攻撃からオープンソースを守るために構築された、レジリエントで長期ガバナンス、コミュニティ主導の AList フォークです。</em></p>\n\n  <img src=\"https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3\" alt=\"latest version\" />\n  <a href=\"https://github.com/OpenListTeam/OpenList/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/OpenListTeam/OpenList\" alt=\"License\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild\"><img src=\"https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main\" alt=\"Build status\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/release/OpenListTeam/OpenList\" alt=\"latest version\" /></a>\n\n  <a href=\"https://github.com/OpenListTeam/OpenList/discussions\"><img src=\"https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936\" alt=\"discussions\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" /></a>\n</div>\n\n---\n\n- [English](./README.md) | [中文](./README_cn.md) | 日本語 | [Dutch](./README_nl.md)\n\n- [コントリビュート](./CONTRIBUTING.md)\n- [行動規範](./CODE_OF_CONDUCT.md)\n- [ライセンス](./LICENSE)\n\n## 免責事項\n\nOpenListは、OpenListチームが独立して維持するオープンソースプロジェクトであり、AGPL-3.0ライセンスに従い、完全なコードの開放性と変更の透明性を維持することに専念しています。\n\nコミュニティ内で、OpenListApp/OpenListAppなど、本プロジェクトと類似した名称を持つサードパーティプロジェクトや、同一または類似した命名を採用する有料専有ソフトウェアが出現していることを確認しています。ユーザーの誤解を避けるため、以下のように宣言いたします：\n\n- OpenListは、いかなるサードパーティ派生プロジェクトとも公式な関連性はありません。\n\n- 本プロジェクトのすべてのソフトウェア、コード、サービスはOpenListチームによって維持され、GitHubで無料で取得できます。\n\n- プロジェクトドキュメントとAPIサービスは主にCloudflareが提供する公益リソースに依存しており、現在有料プランや商業展開はなく、既存機能の使用に費用は発生しません。\n\n私たちはコミュニティの自由な使用と派生開発の権利を尊重しますが、下流プロジェクトに強く呼びかけます：\n\n- 「OpenList」の名前で偽装宣伝や商業利益を得るべきではありません；\n\n- OpenListベースのコードをクローズドソースで配布したり、AGPLライセンス条項に違反してはいけません。\n\nエコシステムの健全な発展をより良く維持するため、以下を推奨します：\n\n- プロジェクトの出典を明確に示し、オープンソース精神に合致する適切なオープンソースライセンスを選択する；\n\n- 商業用途が関わる場合は、「OpenList」や混乱を招く可能性のある名前をプロジェクト名として使用することを避ける；\n\n- OpenListTeam/Logo下の素材を使用する必要がある場合は、協定を遵守した上で修正して使用できます。\n\nOpenListプロジェクトへのご支援とご理解をありがとうございます。\n\n## 特徴\n\n- [x] 複数ストレージ\n  - [x] ローカルストレージ\n  - [x] [Aliyundrive](https://www.alipan.com)\n  - [x] OneDrive / Sharepoint ([グローバル](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [中国](https://portal.partner.microsoftonline.cn), DE, US)\n  - [x] [189cloud](https://cloud.189.cn)（個人、家族）\n  - [x] [GoogleDrive](https://drive.google.com)\n  - [x] [123pan](https://www.123pan.com)\n  - [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)\n  - [x] [PikPak](https://www.mypikpak.com)\n  - [x] [S3](https://aws.amazon.com/s3)\n  - [x] [Seafile](https://seafile.com)\n  - [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)\n  - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)\n  - [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))\n  - [x] [Mediatrack](https://www.mediatrack.cn)\n  - [x] [ProtonDrive](https://proton.me/drive)\n  - [x] [139yun](https://yun.139.com)（個人、家族、グループ）\n  - [x] [YandexDisk](https://disk.yandex.com)\n  - [x] [BaiduNetdisk](http://pan.baidu.com)\n  - [x] [Terabox](https://www.terabox.com/main)\n  - [x] [UC](https://drive.uc.cn)\n  - [x] [Quark](https://pan.quark.cn)\n  - [x] [Thunder](https://pan.xunlei.com)\n  - [x] [Lanzou](https://www.lanzou.com)\n  - [x] [ILanzou](https://www.ilanzou.com)\n  - [x] [Google photo](https://photos.google.com)\n  - [x] [Mega.nz](https://mega.nz)\n  - [x] [Baidu photo](https://photo.baidu.com)\n  - [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)\n  - [x] [115](https://115.com)\n  - [x] [Cloudreve](https://cloudreve.org)\n  - [x] [Dropbox](https://www.dropbox.com)\n  - [x] [FeijiPan](https://www.feijipan.com)\n  - [x] [dogecloud](https://www.dogecloud.com/product/oss)\n  - [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)\n  - [x] [Chaoxing](https://www.chaoxing.com)\n  - [x] [CNB](https://cnb.cool/)\n  - [x] [Degoo](https://degoo.com)\n  - [x] [Doubao](https://www.doubao.com)\n  - [x] [Febbox](https://www.febbox.com)\n  - [x] [GitHub](https://github.com)\n  - [x] [OpenList](https://github.com/OpenListTeam/OpenList)\n  - [x] [Teldrive](https://github.com/tgdrive/teldrive)\n  - [x] [Weiyun](https://www.weiyun.com)\n  - [x] [MediaFire](https://www.mediafire.com)\n- [x] 簡単にデプロイでき、すぐに使える\n- [x] ファイルプレビュー（PDF、markdown、コード、テキストなど）\n- [x] ギャラリーモードでの画像プレビュー\n- [x] ビデオ・オーディオプレビュー、歌詞・字幕対応\n- [x] Officeドキュメントプレビュー（docx、pptx、xlsxなど）\n- [x] `README.md` プレビュー表示\n- [x] ファイルのパーマリンクコピーと直接ダウンロード\n- [x] ダークモード\n- [x] 国際化対応\n- [x] 保護されたルート（パスワード保護と認証）\n- [x] WebDAV\n- [x] Dockerデプロイ\n- [x] Cloudflare Workersプロキシ\n- [x] ファイル/フォルダのパッケージダウンロード\n- [x] Webアップロード（訪問者のアップロード許可可）、削除、フォルダ作成、リネーム、移動、コピー\n- [x] オフラインダウンロード\n- [x] ストレージ間のファイルコピー\n- [x] 単一ファイルのマルチスレッドダウンロード/ストリーム加速\n\n## ドキュメント\n\n- 📘 [グローバルサイト](https://doc.oplist.org)\n- 📚 [バックアップサイト](https://doc.openlist.team)\n- 🌏 [CNサイト](https://doc.oplist.org.cn)\n\n## デモ\n\n- 🌎 [グローバルデモ](https://demo.oplist.org)\n- 🇨🇳 [CNデモ](https://demo.oplist.org.cn)\n\n## ディスカッション\n\n一般的な質問は [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) をご利用ください。***Issues* はバグ報告と機能リクエスト専用です。**\n\n## スポンサー\n\n[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town \"VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.\")\n\n## ライセンス\n\n「OpenList」は [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスの下で公開されているオープンソースソフトウェアです。\n\n## 免責事項\n\n- 本プロジェクトは無料のオープンソースソフトウェアであり、ネットワークディスクを通じたファイル共有を容易にすることを目的とし、主に Go 言語のダウンロードと学習をサポートします。\n- 本ソフトウェアの利用にあたっては、関連する法令を遵守し、不正利用を固く禁じます。\n- 本ソフトウェアは公式 SDK または API に基づいており、その動作を一切改変・破壊・妨害しません。\n- 302 リダイレクトまたはトラフィック転送のみを行い、ユーザーデータの傍受・保存・改ざんは一切行いません。\n- 本プロジェクトは、いかなる公式プラットフォームやサービスプロバイダーとも関係ありません。\n- 本ソフトウェアは「現状有姿」で提供されており、商品性や特定目的への適合性を含むいかなる保証もありません。\n- 本ソフトウェアの使用または使用不能によるいかなる直接的・間接的損害についても、メンテナは責任を負いません。\n- 本ソフトウェアの利用に伴うすべてのリスク（アカウントの凍結やダウンロード速度制限などを含む）は、利用者自身が負うものとします。\n- 本プロジェクトは [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスに従います。詳細は [LICENSE](./LICENSE) ファイルをご覧ください。\n\n## お問い合わせ\n\n- [@GitHub](https://github.com/OpenListTeam)\n- [Telegram グループ](https://t.me/OpenListTeam)\n- [Telegram チャンネル](https://t.me/OpenListOfficial)\n\n## コントリビューター\n\nオリジナルプロジェクト [AlistGo/alist](https://github.com/AlistGo/alist) の作者 [Xhofe](https://github.com/Xhofe) およびその他すべての貢献者に心より感謝いたします。\n\n素晴らしい皆様に感謝します：\n\n[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)\n"
  },
  {
    "path": "README_nl.md",
    "content": "<div align=\"center\">\n  <img style=\"width: 128px; height: 128px;\" src=\"https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg\" alt=\"logo\" />\n\n  <p><em>OpenList is een veerkrachtige, langetermijn, door de gemeenschap geleide fork van AList — gebouwd om open source te beschermen tegen op vertrouwen gebaseerde aanvallen.</em></p>\n\n  <img src=\"https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3\" alt=\"latest version\" />\n  <a href=\"https://github.com/OpenListTeam/OpenList/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/OpenListTeam/OpenList\" alt=\"License\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild\"><img src=\"https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main\" alt=\"Build status\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/release/OpenListTeam/OpenList\" alt=\"latest version\" /></a>\n\n  <a href=\"https://github.com/OpenListTeam/OpenList/discussions\"><img src=\"https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936\" alt=\"discussions\" /></a>\n  <a href=\"https://github.com/OpenListTeam/OpenList/releases\"><img src=\"https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" /></a>\n</div>\n\n---\n\n- [English](./README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | Dutch\n\n- [Bijdragen](./CONTRIBUTING.md)\n- [Gedragscode](./CODE_OF_CONDUCT.md)\n- [Licentie](./LICENSE)\n\n## Disclaimer\n\nOpenList is een open-source project dat onafhankelijk wordt onderhouden door het OpenList Team, volgend op de AGPL-3.0 licentie en toegewijd aan het behouden van volledige code openheid en transparantie van wijzigingen.\n\nWe hebben gemerkt dat er in de gemeenschap enkele derde partij projecten zijn verschenen met namen vergelijkbaar met dit project, zoals OpenListApp/OpenListApp, evenals enkele betaalde eigendomssoftware die dezelfde of soortgelijke naamgeving gebruikt. Om verwarring bij gebruikers te voorkomen, verklaren we hierbij:\n\n- OpenList heeft geen officiële associatie met enige derde partij afgeleide projecten.\n\n- Alle software, code en diensten van dit project worden onderhouden door het OpenList Team en zijn gratis beschikbaar op GitHub.\n\n- Projectdocumentatie en API diensten zijn voornamelijk afhankelijk van liefdadigheidsbronnen verstrekt door Cloudflare. Er zijn momenteel geen betaalplannen of commerciële implementaties, en het gebruik van bestaande functies brengt geen kosten met zich mee.\n\nWe respecteren de rechten van de gemeenschap voor vrij gebruik en afgeleide ontwikkeling, maar we roepen downstream projecten ook ten zeerste op:\n\n- Mogen niet de \"OpenList\" naam gebruiken voor namaakpromotie of commercieel gewin;\n\n- Mogen OpenList-gebaseerde code niet distribueren op een closed-source manier of AGPL licentievoorwaarden schenden.\n\nOm een gezonde ecosysteemontwikkeling beter te onderhouden, bevelen we aan:\n\n- Duidelijk de projectbron aangeven en passende open-source licenties kiezen in overeenstemming met de open-source geest;\n\n- Bij commercieel gebruik, vermijd het gebruik van \"OpenList\" of enige verwarrende naamgeving als projectnaam;\n\n- Als u materialen onder OpenListTeam/Logo moet gebruiken, kunt u deze wijzigen en gebruiken onder naleving van de overeenkomst.\n\nDank u voor uw ondersteuning en begrip\n\n## Functies\n\n- [x] Meerdere opslagmogelijkheden\n  - [x] Lokale opslag\n  - [x] [Aliyundrive](https://www.alipan.com)\n  - [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)\n  - [x] [189cloud](https://cloud.189.cn) (Persoonlijk, Familie)\n  - [x] [GoogleDrive](https://drive.google.com)\n  - [x] [123pan](https://www.123pan.com)\n  - [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)\n  - [x] [PikPak](https://www.mypikpak.com)\n  - [x] [S3](https://aws.amazon.com/s3)\n  - [x] [Seafile](https://seafile.com)\n  - [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)\n  - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)\n  - [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))\n  - [x] [MediaFire](https://www.mediafire.com)\n  - [x] [Mediatrack](https://www.mediatrack.cn)\n  - [x] [ProtonDrive](https://proton.me/drive)\n  - [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)\n  - [x] [YandexDisk](https://disk.yandex.com)\n  - [x] [BaiduNetdisk](http://pan.baidu.com)\n  - [x] [Terabox](https://www.terabox.com/main)\n  - [x] [UC](https://drive.uc.cn)\n  - [x] [Quark](https://pan.quark.cn)\n  - [x] [Thunder](https://pan.xunlei.com)\n  - [x] [Lanzou](https://www.lanzou.com)\n  - [x] [ILanzou](https://www.ilanzou.com)\n  - [x] [Google photo](https://photos.google.com)\n  - [x] [Mega.nz](https://mega.nz)\n  - [x] [Baidu photo](https://photo.baidu.com)\n  - [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)\n  - [x] [115](https://115.com)\n  - [x] [Cloudreve](https://cloudreve.org)\n  - [x] [Dropbox](https://www.dropbox.com)\n  - [x] [FeijiPan](https://www.feijipan.com)\n  - [x] [dogecloud](https://www.dogecloud.com/product/oss)\n  - [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)\n  - [x] [Chaoxing](https://www.chaoxing.com)\n  - [x] [CNB](https://cnb.cool/)\n  - [x] [Degoo](https://degoo.com)\n  - [x] [Doubao](https://www.doubao.com)\n  - [x] [Febbox](https://www.febbox.com)\n  - [x] [GitHub](https://github.com)\n  - [x] [OpenList](https://github.com/OpenListTeam/OpenList)\n  - [x] [Teldrive](https://github.com/tgdrive/teldrive)\n  - [x] [Weiyun](https://www.weiyun.com)\n- [x] Eenvoudig te implementeren en direct te gebruiken\n- [x] Bestandsvoorbeeld (PDF, markdown, code, platte tekst, ...)\n- [x] Afbeeldingsvoorbeeld in galerijweergave\n- [x] Video- en audiovoorbeeld, ondersteuning voor songteksten en ondertitels\n- [x] Office-documenten voorbeeld (docx, pptx, xlsx, ...)\n- [x] `README.md` voorbeeldweergave\n- [x] Permalink kopiëren en direct downloaden van bestanden\n- [x] Donkere modus\n- [x] I18n\n- [x] Beschermde routes (wachtwoordbeveiliging en authenticatie)\n- [x] WebDAV\n- [x] Docker implementatie\n- [x] Cloudflare Workers proxy\n- [x] Bestands-/map-pakket download\n- [x] Webupload (bezoekers kunnen uploaden toestaan), verwijderen, map aanmaken, hernoemen, verplaatsen en kopiëren\n- [x] Offline download\n- [x] Bestanden kopiëren tussen twee opslaglocaties\n- [x] Multi-thread downloadversnelling voor enkelvoudige download/stream\n\n## Documentatie\n\n- 📘 [Global Site](https://doc.oplist.org)\n- 📚 [Backup Site](https://doc.openlist.team)\n- 🌏 [CN Site](https://doc.oplist.org.cn)\n\n## Demo\n\n- 🌎 [Global Demo](https://demo.oplist.org)\n- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)\n\n## Discussie\n\nStel algemene vragen in [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions), ***Issues* zijn alleen voor bugmeldingen en feature requests.**\n\n## Sponsoren\n\n[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town \"VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.\")\n\n## Licentie\n\n`OpenList` is open-source software onder de [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) licentie.\n\n## Disclaimer\n\n- Dit project is gratis en open-source software, ontworpen om het delen van bestanden via netdisks te vergemakkelijken, voornamelijk bedoeld ter ondersteuning van het downloaden en leren van de programmeertaal Go.\n- Houd u bij het gebruik van deze software aan alle toepasselijke wetten en voorschriften. Elk misbruik is ten strengste verboden.\n- De software is gebaseerd op officiële SDK's of API's zonder enige wijziging, verstoring of beïnvloeding van hun gedrag.\n- Het voert alleen HTTP 302-omleidingen of verkeersdoorsturing uit en onderschept, slaat of wijzigt geen gebruikersgegevens.\n- Dit project is niet gelieerd aan enig officieel platform of dienstverlener.\n- De software wordt geleverd \"zoals deze is\", zonder enige vorm van garantie, expliciet of impliciet, inclusief maar niet beperkt tot garanties van verkoopbaarheid of geschiktheid voor een bepaald doel.\n- De beheerders zijn niet aansprakelijk voor enige directe of indirecte schade die voortvloeit uit het gebruik van of het onvermogen om deze software te gebruiken.\n- U bent zelf verantwoordelijk voor alle risico's die gepaard gaan met het gebruik van deze software, inclusief maar niet beperkt tot accountblokkades of downloadbeperkingen.\n- Dit project valt onder de [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) licentie. Zie het [LICENSE](./LICENSE) bestand voor details.\n\n## Contact\n\n- [@GitHub](https://github.com/OpenListTeam)\n- [Telegram Groep](https://t.me/OpenListTeam)\n- [Telegram Kanaal](https://t.me/OpenListOfficial)\n\n## Bijdragers\n\nWij danken de auteur [Xhofe](https://github.com/Xhofe) van het originele project [AlistGo/alist](https://github.com/AlistGo/alist) en alle andere bijdragers.\n\nDank aan deze geweldige mensen:\n\n[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nOnly the latest stable release receives security patches. We strongly recommend always keeping OpenList up to date.\n\n| Version              | Supported          |\n| -------------------- | ------------------ |\n| Latest stable (v4.x) | :white_check_mark: |\n| Older versions       | :x:                |\n\n## Reporting a Vulnerability\n\n**Please do NOT report security vulnerabilities through public GitHub Issues.**\n\nIf you discover a security vulnerability in OpenList, please report it responsibly by using one of the following channels:\n\n- **GitHub Private Security Advisory** (preferred): [Submit here](https://github.com/OpenListTeam/OpenList/security/advisories/new)\n- **Telegram**: Contact a maintainer privately via [@OpenListTeam](https://t.me/OpenListTeam)\n\nWhen reporting, please include as much of the following as possible:\n\n- A description of the vulnerability and its potential impact\n- The affected version(s)\n- Step-by-step instructions to reproduce the issue\n- Any proof-of-concept code or screenshots (if applicable)\n- Suggested mitigation or fix (optional but appreciated)\n\n## Security Best Practices for Users\n\nTo keep your OpenList instance secure:\n\n- Always update to the latest release.\n- Use a strong, unique admin password and change it after first login.\n- Enable HTTPS (TLS) for your deployment — do **not** expose OpenList over plain HTTP on the public internet.\n- Limit exposed ports using a reverse proxy (e.g., Nginx, Caddy).\n- Set up access controls and avoid enabling guest access unless necessary.\n- Regularly review mounted storage permissions and revoke unused API tokens.\n- When using Docker, avoid running the container as root if possible.\n\n## Acknowledgments\n\nWe sincerely thank all security researchers and community members who responsibly disclose vulnerabilities and help make OpenList safer for everyone.\n\n---\n\n# 安全政策\n\n## 支持的版本\n\n我们仅对最新稳定版本提供安全补丁。强烈建议始终保持 OpenList 为最新版本。\n\n| 版本               | 是否支持           |\n| ------------------ | ------------------ |\n| 最新稳定版（v4.x） | :white_check_mark: |\n| 旧版本             | :x:                |\n\n## 报告漏洞\n\n**请勿通过公开的 GitHub Issues 报告安全漏洞。**\n\n如果您在 OpenList 中发现安全漏洞，请通过以下渠道之一负责任地进行报告：\n\n- **GitHub 私密安全公告**（推荐）：[点击提交](https://github.com/OpenListTeam/OpenList/security/advisories/new)\n- **Telegram**：通过 [@OpenListTeam](https://t.me/OpenListTeam) 私信联系维护者\n\n报告时，请尽量提供以下信息：\n\n- 漏洞描述及其潜在影响\n- 受影响的版本\n- 复现问题的详细步骤\n- 概念验证代码或截图（如有）\n- 建议的缓解措施或修复方案（可选，但非常欢迎）\n\n## 用户安全最佳实践\n\n为保障您的 OpenList 实例安全：\n\n- 始终更新至最新版本。\n- 使用强且唯一的管理员密码，并在首次登录后立即修改。\n- 为您的部署启用 HTTPS（TLS）—— **请勿**在公网上以明文 HTTP 方式暴露 OpenList。\n- 使用反向代理（如 Nginx、Caddy）限制对外暴露的端口。\n- 配置访问控制，非必要情况下不要开启访客访问。\n- 定期检查已挂载存储的权限，并撤销未使用的 API 令牌。\n- 使用 Docker 部署时，尽可能避免以 root 用户运行容器。\n\n## 致谢\n\n我们衷心感谢所有负责任地披露漏洞、帮助 OpenList 变得更加安全的安全研究人员和社区成员。\n"
  },
  {
    "path": "build.sh",
    "content": "set -e\nappName=\"openlist\"\nbuiltAt=\"$(date +'%F %T %z')\"\ngitAuthor=\"The OpenList Projects Contributors <noreply@openlist.team>\"\ngitCommit=$(git log --pretty=format:\"%h\" -1)\n\n# Set frontend repository, default to OpenListTeam/OpenList-Frontend\nfrontendRepo=\"${FRONTEND_REPO:-OpenListTeam/OpenList-Frontend}\"\n\ngithubAuthArgs=\"\"\nif [ -n \"$GITHUB_TOKEN\" ]; then\n  githubAuthArgs=\"--header \\\"Authorization: Bearer $GITHUB_TOKEN\\\"\"\nfi\n\n# Check for lite parameter\nuseLite=false\nif [[ \"$*\" == *\"lite\"* ]]; then\n  useLite=true\nfi\n\nif [ \"$1\" = \"dev\" ]; then\n  version=\"dev\"\n  webVersion=\"rolling\"\nelif [ \"$1\" = \"beta\" ]; then\n  version=\"beta\"\n  webVersion=\"rolling\"\nelse\n  git tag -d beta || true\n  # Always true if there's no tag\n  version=$(git describe --abbrev=0 --tags 2>/dev/null || echo \"v0.0.0\")\n  webVersion=$(eval \"curl -fsSL --max-time 2 $githubAuthArgs \\\"https://api.github.com/repos/$frontendRepo/releases/latest\\\"\" | grep \"tag_name\" | head -n 1 | awk -F \":\" '{print $2}' | sed 's/\\\"//g;s/,//g;s/ //g')\nfi\n\necho \"backend version: $version\"\necho \"frontend version: $webVersion\"\nif [ \"$useLite\" = true ]; then\n  echo \"using lite frontend\"\nelse\n  echo \"using standard frontend\"\nfi\n\nldflags=\"\\\n-w -s \\\n-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$builtAt' \\\n-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=$gitAuthor' \\\n-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$gitCommit' \\\n-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$version' \\\n-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=$webVersion' \\\n\"\n\nFetchWebRolling() {\n  pre_release_json=$(eval \"curl -fsSL --max-time 2 $githubAuthArgs -H \\\"Accept: application/vnd.github.v3+json\\\" \\\"https://api.github.com/repos/$frontendRepo/releases/tags/rolling\\\"\")\n  pre_release_assets=$(echo \"$pre_release_json\" | jq -r '.assets[].browser_download_url')\n  \n  # There is no lite for rolling\n  pre_release_tar_url=$(echo \"$pre_release_assets\" | grep \"openlist-frontend-dist\" | grep -v \"lite\" | grep \"\\.tar\\.gz$\")\n\n  curl -fsSL \"$pre_release_tar_url\" -o dist.tar.gz\n  rm -rf public/dist && mkdir -p public/dist\n  tar -zxvf dist.tar.gz -C public/dist\n  rm -rf dist.tar.gz\n}\n\nFetchWebRelease() {\n  release_json=$(eval \"curl -fsSL --max-time 2 $githubAuthArgs -H \\\"Accept: application/vnd.github.v3+json\\\" \\\"https://api.github.com/repos/$frontendRepo/releases/latest\\\"\")\n  release_assets=$(echo \"$release_json\" | jq -r '.assets[].browser_download_url')\n  \n  if [ \"$useLite\" = true ]; then\n    release_tar_url=$(echo \"$release_assets\" | grep \"openlist-frontend-dist-lite\" | grep \"\\.tar\\.gz$\")\n  else\n    release_tar_url=$(echo \"$release_assets\" | grep \"openlist-frontend-dist\" | grep -v \"lite\" | grep \"\\.tar\\.gz$\")\n  fi\n  \n  curl -fsSL \"$release_tar_url\" -o dist.tar.gz\n  rm -rf public/dist && mkdir -p public/dist\n  tar -zxvf dist.tar.gz -C public/dist\n  rm -rf dist.tar.gz\n}\n\nBuildWinArm64() {\n  echo building for windows-arm64\n  chmod +x ./wrapper/zcc-arm64\n  chmod +x ./wrapper/zcxx-arm64\n  export GOOS=windows\n  export GOARCH=arm64\n  export CC=$(pwd)/wrapper/zcc-arm64\n  export CXX=$(pwd)/wrapper/zcxx-arm64\n  export CGO_ENABLED=1\n  go build -o \"$1\" -ldflags=\"$ldflags\" -tags=jsoniter .\n}\n\nBuildWin7() {\n  # Setup Win7 Go compiler (patched version that supports Windows 7)\n  go_version=$(go version | grep -o 'go[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+' | sed 's/go//')\n  echo \"Detected Go version: $go_version\"\n  \n  curl -fsSL --retry 3 -o go-win7.zip -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n    \"https://github.com/XTLS/go-win7/releases/download/patched-${go_version}/go-for-win7-linux-amd64.zip\"\n  \n  rm -rf go-win7\n  unzip go-win7.zip -d go-win7\n  rm go-win7.zip\n  \n  # Set permissions for all wrapper files\n  chmod +x ./wrapper/zcc-win7\n  chmod +x ./wrapper/zcxx-win7\n  chmod +x ./wrapper/zcc-win7-386\n  chmod +x ./wrapper/zcxx-win7-386\n  \n  # Build for both 386 and amd64 architectures\n  for arch in \"386\" \"amd64\"; do\n    echo \"building for windows7-${arch}\"\n    export GOOS=windows\n    export GOARCH=${arch}\n    export CGO_ENABLED=1\n    \n    # Use architecture-specific wrapper files\n    if [ \"$arch\" = \"386\" ]; then\n      export CC=$(pwd)/wrapper/zcc-win7-386\n      export CXX=$(pwd)/wrapper/zcxx-win7-386\n    else\n      export CC=$(pwd)/wrapper/zcc-win7\n      export CXX=$(pwd)/wrapper/zcxx-win7\n    fi\n    \n    # Use the patched Go compiler for Win7 compatibility\n    $(pwd)/go-win7/bin/go build -o \"${1}-${arch}.exe\" -ldflags=\"$ldflags\" -tags=jsoniter .\n  done\n}\n\nBuildDev() {\n  rm -rf .git/\n  mkdir -p \"dist\"\n  muslflags=\"--extldflags '-static -fpic' $ldflags\"\n  BASE=\"https://github.com/OpenListTeam/musl-compilers/releases/latest/download/\"\n  FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    curl -fsSL -o \"${i}.tgz\" \"${url}\"\n    sudo tar xf \"${i}.tgz\" --strip-components 1 -C /usr/local\n  done\n  OS_ARCHES=(linux-musl-amd64 linux-musl-arm64)\n  CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc)\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=${CGO_ARGS[$i]}\n    echo building for ${os_arch}\n    export GOOS=${os_arch%%-*}\n    export GOARCH=${os_arch##*-}\n    export CC=${cgo_cc}\n    export CGO_ENABLED=1\n    go build -o ./dist/$appName-$os_arch -ldflags=\"$muslflags\" -tags=jsoniter .\n  done\n  xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out \"$appName\" -ldflags=\"$ldflags\" -tags=jsoniter .\n  mv \"$appName\"-* dist\n  cd dist\n  # cp ./\"$appName\"-windows-amd64.exe ./\"$appName\"-windows-amd64-upx.exe\n  # upx -9 ./\"$appName\"-windows-amd64-upx.exe\n  find . -type f -print0 | xargs -0 md5sum >md5.txt\n  cat md5.txt\n}\n\nBuildDocker() {\n  go build -o ./bin/\"$appName\" -ldflags=\"$ldflags\" -tags=jsoniter .\n}\n\nPrepareBuildDockerMusl() {\n  mkdir -p build/musl-libs\n  BASE=\"https://github.com/OpenListTeam/musl-compilers/releases/latest/download/\"\n  FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross loongarch64-linux-musl-cross) ## Disable s390x-linux-musl-cross builds\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    lib_tgz=\"build/${i}.tgz\"\n    curl -fsSL -o \"${lib_tgz}\" \"${url}\"\n    tar xf \"${lib_tgz}\" --strip-components 1 -C build/musl-libs\n    rm -f \"${lib_tgz}\"\n  done\n}\n\nBuildDockerMultiplatform() {\n  go mod download\n\n  # run PrepareBuildDockerMusl before build\n  export PATH=$PATH:$PWD/build/musl-libs/bin\n\n  docker_lflags=\"--extldflags '-static -fpic' $ldflags\"\n  export CGO_ENABLED=1\n\n  OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-riscv64 linux-ppc64le linux-loong64) ## Disable linux-s390x builds\n  CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc loongarch64-linux-musl-gcc) ## Disable s390x-linux-musl-gcc builds\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=${CGO_ARGS[$i]}\n    os=${os_arch%%-*}\n    arch=${os_arch##*-}\n    export GOOS=$os\n    export GOARCH=$arch\n    export CC=${cgo_cc}\n    echo \"building for $os_arch\"\n    go build -o build/$os/$arch/\"$appName\" -ldflags=\"$docker_lflags\" -tags=jsoniter .\n  done\n\n  DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)\n  CGO_ARGS=(armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc)\n  GO_ARM=(6 7)\n  export GOOS=linux\n  export GOARCH=arm\n  for i in \"${!DOCKER_ARM_ARCHES[@]}\"; do\n    docker_arch=${DOCKER_ARM_ARCHES[$i]}\n    cgo_cc=${CGO_ARGS[$i]}\n    export GOARM=${GO_ARM[$i]}\n    export CC=${cgo_cc}\n    echo \"building for $docker_arch\"\n    go build -o build/${docker_arch%%-*}/${docker_arch##*-}/\"$appName\" -ldflags=\"$docker_lflags\" -tags=jsoniter .\n  done\n}\n\nBuildRelease() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  BuildWinArm64 ./build/\"$appName\"-windows-arm64.exe\n  BuildWin7 ./build/\"$appName\"-windows7\n  xgo -out \"$appName\" -ldflags=\"$ldflags\" -tags=jsoniter .\n  # why? Because some target platforms seem to have issues with upx compression\n  # upx -9 ./\"$appName\"-linux-amd64\n  # cp ./\"$appName\"-windows-amd64.exe ./\"$appName\"-windows-amd64-upx.exe\n  # upx -9 ./\"$appName\"-windows-amd64-upx.exe\n  mv \"$appName\"-* build\n  \n  # Build LoongArch with glibc (both old world abi1.0 and new world abi2.0)\n  # Separate from musl builds to avoid cache conflicts\n  BuildLoongGLIBC ./build/$appName-linux-loong64-abi1.0 abi1.0\n  BuildLoongGLIBC ./build/$appName-linux-loong64 abi2.0\n}\n\nBuildLoongGLIBC() {\n  local target_abi=\"$2\"\n  local output_file=\"$1\"\n  local oldWorldGoVersion=\"1.25.0\"\n  \n  if [ \"$target_abi\" = \"abi1.0\" ]; then\n    echo building for linux-loong64-abi1.0\n  else\n    echo building for linux-loong64-abi2.0\n    target_abi=\"abi2.0\"  # Default to abi2.0 if not specified\n  fi\n  \n  # Note: No longer need global cache cleanup since ABI1.0 uses isolated cache directory\n  echo \"Using optimized cache strategy: ABI1.0 has isolated cache, ABI2.0 uses standard cache\"\n  \n  if [ \"$target_abi\" = \"abi1.0\" ]; then\n    # Setup abi1.0 toolchain and patched Go compiler similar to cgo-action implementation\n    echo \"Setting up Loongson old-world ABI1.0 toolchain and patched Go compiler...\"\n    \n    # Download and setup patched Go compiler for old-world\n    if ! curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n      \"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz\" \\\n      -o go-loong64-abi1.0.tar.gz; then\n      echo \"Error: Failed to download patched Go compiler for old-world ABI1.0\"\n      if [ -n \"$GITHUB_TOKEN\" ]; then\n        echo \"Error output from curl:\"\n        curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n          \"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz\" \\\n          -o go-loong64-abi1.0.tar.gz || true\n      fi\n      return 1\n    fi\n    \n    rm -rf go-loong64-abi1.0\n    mkdir go-loong64-abi1.0\n    if ! tar -xzf go-loong64-abi1.0.tar.gz -C go-loong64-abi1.0 --strip-components=1; then\n      echo \"Error: Failed to extract patched Go compiler\"\n      return 1\n    fi\n    rm go-loong64-abi1.0.tar.gz\n    \n    # Download and setup GCC toolchain for old-world\n    if ! curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n      \"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/loongson-gnu-toolchain-8.3.novec-x86_64-loongarch64-linux-gnu-rc1.1.tar.xz\" \\\n      -o gcc8-loong64-abi1.0.tar.xz; then\n      echo \"Error: Failed to download GCC toolchain for old-world ABI1.0\"\n      if [ -n \"$GITHUB_TOKEN\" ]; then\n        echo \"Error output from curl:\"\n        curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n          \"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/loongson-gnu-toolchain-8.3.novec-x86_64-loongarch64-linux-gnu-rc1.1.tar.xz\" \\\n          -o gcc8-loong64-abi1.0.tar.xz || true\n      fi\n      return 1\n    fi\n    \n    rm -rf gcc8-loong64-abi1.0\n    mkdir gcc8-loong64-abi1.0\n    if ! tar -Jxf gcc8-loong64-abi1.0.tar.xz -C gcc8-loong64-abi1.0 --strip-components=1; then\n      echo \"Error: Failed to extract GCC toolchain\"\n      return 1\n    fi\n    rm gcc8-loong64-abi1.0.tar.xz\n    \n    # Setup separate cache directory for ABI1.0 to avoid cache pollution\n    abi1_cache_dir=\"$(pwd)/go-loong64-abi1.0-cache\"\n    mkdir -p \"$abi1_cache_dir\"\n    echo \"Using separate cache directory for ABI1.0: $abi1_cache_dir\"\n    \n    # Use patched Go compiler for old-world build (critical for ABI1.0 compatibility)\n    echo \"Building with patched Go compiler for old-world ABI1.0...\"\n    echo \"Using isolated cache directory: $abi1_cache_dir\"\n    \n    # Use env command to set environment variables locally without affecting global environment\n    if ! env GOOS=linux GOARCH=loong64 \\\n        CC=\"$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc\" \\\n        CXX=\"$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++\" \\\n        CGO_ENABLED=1 \\\n        GOCACHE=\"$abi1_cache_dir\" \\\n        $(pwd)/go-loong64-abi1.0/bin/go build -a -o \"$output_file\" -ldflags=\"$ldflags\" -tags=jsoniter .; then\n      echo \"Error: Build failed with patched Go compiler\"\n      echo \"Attempting retry with cache cleanup...\"\n      env GOCACHE=\"$abi1_cache_dir\" $(pwd)/go-loong64-abi1.0/bin/go clean -cache\n      if ! env GOOS=linux GOARCH=loong64 \\\n          CC=\"$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc\" \\\n          CXX=\"$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++\" \\\n          CGO_ENABLED=1 \\\n          GOCACHE=\"$abi1_cache_dir\" \\\n          $(pwd)/go-loong64-abi1.0/bin/go build -a -o \"$output_file\" -ldflags=\"$ldflags\" -tags=jsoniter .; then\n        echo \"Error: Build failed again after cache cleanup\"\n        echo \"Build environment details:\"\n        echo \"GOOS=linux\"\n        echo \"GOARCH=loong64\" \n        echo \"CC=$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc\"\n        echo \"CXX=$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++\"\n        echo \"CGO_ENABLED=1\"\n        echo \"GOCACHE=$abi1_cache_dir\"\n        echo \"Go version: $($(pwd)/go-loong64-abi1.0/bin/go version)\"\n        echo \"GCC version: $($(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc --version | head -1)\"\n        return 1\n      fi\n    fi\n  else\n    # Setup abi2.0 toolchain for new world glibc build\n    echo \"Setting up new-world ABI2.0 toolchain...\"\n    if ! curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n      \"https://github.com/loong64/cross-tools/releases/download/20250507/x86_64-cross-tools-loongarch64-unknown-linux-gnu-legacy.tar.xz\" \\\n      -o gcc12-loong64-abi2.0.tar.xz; then\n      echo \"Error: Failed to download GCC toolchain for new-world ABI2.0\"\n      if [ -n \"$GITHUB_TOKEN\" ]; then\n        echo \"Error output from curl:\"\n        curl -fsSL --retry 3 -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n          \"https://github.com/loong64/cross-tools/releases/download/20250507/x86_64-cross-tools-loongarch64-unknown-linux-gnu-legacy.tar.xz\" \\\n          -o gcc12-loong64-abi2.0.tar.xz || true\n      fi\n      return 1\n    fi\n    \n    rm -rf gcc12-loong64-abi2.0\n    mkdir gcc12-loong64-abi2.0\n    if ! tar -Jxf gcc12-loong64-abi2.0.tar.xz -C gcc12-loong64-abi2.0 --strip-components=1; then\n      echo \"Error: Failed to extract GCC toolchain\"\n      return 1\n    fi\n    rm gcc12-loong64-abi2.0.tar.xz\n    \n    export GOOS=linux\n    export GOARCH=loong64\n    export CC=$(pwd)/gcc12-loong64-abi2.0/bin/loongarch64-unknown-linux-gnu-gcc\n    export CXX=$(pwd)/gcc12-loong64-abi2.0/bin/loongarch64-unknown-linux-gnu-g++\n    export CGO_ENABLED=1\n    \n    # Use standard Go compiler for new-world build\n    echo \"Building with standard Go compiler for new-world ABI2.0...\"\n    if ! go build -a -o \"$output_file\" -ldflags=\"$ldflags\" -tags=jsoniter .; then\n      echo \"Error: Build failed with standard Go compiler\"\n      echo \"Attempting retry with cache cleanup...\"\n      go clean -cache\n      if ! go build -a -o \"$output_file\" -ldflags=\"$ldflags\" -tags=jsoniter .; then\n        echo \"Error: Build failed again after cache cleanup\"\n        echo \"Build environment details:\"\n        echo \"GOOS=$GOOS\"\n        echo \"GOARCH=$GOARCH\"\n        echo \"CC=$CC\"\n        echo \"CXX=$CXX\"\n        echo \"CGO_ENABLED=$CGO_ENABLED\"\n        echo \"Go version: $(go version)\"\n        echo \"GCC version: $($CC --version | head -1)\"\n        return 1\n      fi\n    fi\n  fi\n}\n\nBuildReleaseLinuxMusl() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  muslflags=\"--extldflags '-static -fpic' $ldflags\"\n  BASE=\"https://github.com/OpenListTeam/musl-compilers/releases/latest/download/\"\n  FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross loongarch64-linux-musl-cross)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    curl -fsSL -o \"${i}.tgz\" \"${url}\"\n    sudo tar xf \"${i}.tgz\" --strip-components 1 -C /usr/local\n    rm -f \"${i}.tgz\"\n  done\n  OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x linux-musl-loong64)\n  CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc loongarch64-linux-musl-gcc)\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=${CGO_ARGS[$i]}\n    echo building for ${os_arch}\n    export GOOS=${os_arch%%-*}\n    export GOARCH=${os_arch##*-}\n    export CC=${cgo_cc}\n    export CGO_ENABLED=1\n    go build -o ./build/$appName-$os_arch -ldflags=\"$muslflags\" -tags=jsoniter .\n  done\n}\n\nBuildReleaseLinuxMuslArm() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  muslflags=\"--extldflags '-static -fpic' $ldflags\"\n  BASE=\"https://github.com/OpenListTeam/musl-compilers/releases/latest/download/\"\n  FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    curl -fsSL -o \"${i}.tgz\" \"${url}\"\n    sudo tar xf \"${i}.tgz\" --strip-components 1 -C /usr/local\n    rm -f \"${i}.tgz\"\n  done\n  OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r)\n  CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc)\n  GOARMS=('' '' '' '' '5' '5' '6' '6' '7' '7' '7')\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=${CGO_ARGS[$i]}\n    arm=${GOARMS[$i]}\n    echo building for ${os_arch}\n    export GOOS=linux\n    export GOARCH=arm\n    export CC=${cgo_cc}\n    export CGO_ENABLED=1\n    export GOARM=${arm}\n    go build -o ./build/$appName-$os_arch -ldflags=\"$muslflags\" -tags=jsoniter .\n  done\n}\n\n\nBuildReleaseAndroid() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip\n  unzip android-ndk-r26b-linux.zip\n  rm android-ndk-r26b-linux.zip\n  OS_ARCHES=(amd64 arm64 386 arm)\n  CGO_ARGS=(x86_64-linux-android24-clang aarch64-linux-android24-clang i686-linux-android24-clang armv7a-linux-androideabi24-clang)\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=$(realpath android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/${CGO_ARGS[$i]})\n    echo building for android-${os_arch}\n    export GOOS=android\n    export GOARCH=${os_arch##*-}\n    export CC=${cgo_cc}\n    export CGO_ENABLED=1\n    go build -o ./build/$appName-android-$os_arch -ldflags=\"$ldflags\" -tags=jsoniter .\n    android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip ./build/$appName-android-$os_arch\n  done\n}\n\nBuildReleaseFreeBSD() {\n  rm -rf .git/\n  mkdir -p \"build/freebsd\"\n  \n  # Get latest FreeBSD 14.x release version from GitHub \n  freebsd_version=$(eval \"curl -fsSL --max-time 2 $githubAuthArgs \\\"https://api.github.com/repos/freebsd/freebsd-src/tags\\\"\" | \\\n    jq -r '.[].name' | \\\n    grep '^release/14\\.' | \\\n    grep -v -- '-p[0-9]*$' | \\\n    sort -V | \\\n    tail -1 | \\\n    sed 's/release\\///' | \\\n    sed 's/\\.0$//')\n  \n  if [ -z \"$freebsd_version\" ]; then\n    echo \"Failed to get FreeBSD version, falling back to 14.3\"\n    freebsd_version=\"14.3\"\n  fi\n\n  echo \"Using FreeBSD version: $freebsd_version\"\n  \n  OS_ARCHES=(amd64 arm64 i386)\n  GO_ARCHES=(amd64 arm64 386)\n  CGO_ARGS=(x86_64-unknown-freebsd${freebsd_version} aarch64-unknown-freebsd${freebsd_version} i386-unknown-freebsd${freebsd_version})\n  for i in \"${!OS_ARCHES[@]}\"; do\n    os_arch=${OS_ARCHES[$i]}\n    cgo_cc=\"clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}\"\n    echo building for freebsd-${os_arch}\n    sudo mkdir -p \"/opt/freebsd/${os_arch}\"\n    wget -q https://download.freebsd.org/releases/${os_arch}/${freebsd_version}-RELEASE/base.txz\n    sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch}\n    rm base.txz\n    export GOOS=freebsd\n    export GOARCH=${GO_ARCHES[$i]}\n    export CC=${cgo_cc}\n    export CGO_ENABLED=1\n    export CGO_LDFLAGS=\"-fuse-ld=lld\"\n    go build -o ./build/$appName-freebsd-$os_arch -ldflags=\"$ldflags\" -tags=jsoniter .\n  done\n}\n\nMakeRelease() {\n  cd build\n  if [ -d compress ]; then\n    rm -rv compress\n  fi\n  mkdir compress\n  \n  # Add -lite suffix if useLite is true\n  liteSuffix=\"\"\n  if [ \"$useLite\" = true ]; then\n    liteSuffix=\"-lite\"\n  fi\n  \n  for i in $(find . -type f -name \"$appName-linux-*\"); do\n    cp \"$i\" \"$appName\"\n    tar -czvf compress/\"$i$liteSuffix\".tar.gz \"$appName\"\n    rm -f \"$appName\"\n  done\n    for i in $(find . -type f -name \"$appName-android-*\"); do\n    cp \"$i\" \"$appName\"\n    tar -czvf compress/\"$i$liteSuffix\".tar.gz \"$appName\"\n    rm -f \"$appName\"\n  done\n  for i in $(find . -type f -name \"$appName-darwin-*\"); do\n    cp \"$i\" \"$appName\"\n    tar -czvf compress/\"$i$liteSuffix\".tar.gz \"$appName\"\n    rm -f \"$appName\"\n  done\n  for i in $(find . -type f -name \"$appName-freebsd-*\"); do\n    cp \"$i\" \"$appName\"\n    tar -czvf compress/\"$i$liteSuffix\".tar.gz \"$appName\"\n    rm -f \"$appName\"\n  done\n  for i in $(find . -type f \\( -name \"$appName-windows-*\" -o -name \"$appName-windows7-*\" \\)); do\n    cp \"$i\" \"$appName\".exe\n    zip compress/$(echo $i | sed 's/\\.[^.]*$//')$liteSuffix.zip \"$appName\".exe\n    rm -f \"$appName\".exe\n  done\n  cd compress\n  \n  # Handle MD5 filename - add -lite suffix only if not already present\n  md5FileName=\"$1\"\n  if [ \"$useLite\" = true ] && [[ \"$1\" != *\"-lite.txt\" ]]; then\n    md5FileName=$(echo \"$1\" | sed 's/\\.txt$/-lite.txt/')\n  fi\n  \n  find . -type f -print0 | xargs -0 md5sum >\"$md5FileName\"\n  cat \"$md5FileName\"\n  cd ../..\n}\n\n# Parse parameters to handle lite parameter position flexibility\nbuildType=\"\"\ndockerType=\"\"\notherParam=\"\"\n\nfor arg in \"$@\"; do\n  case $arg in\n    dev|beta|release|zip|prepare)\n      if [ -z \"$buildType\" ]; then\n        buildType=\"$arg\"\n      fi\n      ;;\n    docker|docker-multiplatform|linux_musl_arm|linux_musl|android|freebsd|web)\n      if [ -z \"$dockerType\" ]; then\n        dockerType=\"$arg\"\n      fi\n      ;;\n    lite)\n      # lite parameter is already handled above\n      ;;\n    *)\n      if [ -z \"$otherParam\" ]; then\n        otherParam=\"$arg\"\n      fi\n      ;;\n  esac\ndone\n\nif [ \"$buildType\" = \"dev\" ]; then\n  FetchWebRolling\n  if [ \"$dockerType\" = \"docker\" ]; then\n    BuildDocker\n  elif [ \"$dockerType\" = \"docker-multiplatform\" ]; then\n      BuildDockerMultiplatform\n  elif [ \"$dockerType\" = \"web\" ]; then\n    echo \"web only\"\n  else\n    BuildDev\n  fi\nelif [ \"$buildType\" = \"release\" -o \"$buildType\" = \"beta\" ]; then\n  if [ \"$buildType\" = \"beta\" ]; then\n    FetchWebRolling\n  else\n    FetchWebRelease\n  fi\n  if [ \"$dockerType\" = \"docker\" ]; then\n    BuildDocker\n  elif [ \"$dockerType\" = \"docker-multiplatform\" ]; then\n    BuildDockerMultiplatform\n  elif [ \"$dockerType\" = \"linux_musl_arm\" ]; then\n    BuildReleaseLinuxMuslArm\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-linux-musl-arm-lite.txt\"\n    else\n      MakeRelease \"md5-linux-musl-arm.txt\"\n    fi\n  elif [ \"$dockerType\" = \"linux_musl\" ]; then\n    BuildReleaseLinuxMusl\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-linux-musl-lite.txt\"\n    else\n      MakeRelease \"md5-linux-musl.txt\"\n    fi\n  elif [ \"$dockerType\" = \"android\" ]; then\n    BuildReleaseAndroid\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-android-lite.txt\"\n    else\n      MakeRelease \"md5-android.txt\"\n    fi\n  elif [ \"$dockerType\" = \"freebsd\" ]; then\n    BuildReleaseFreeBSD\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-freebsd-lite.txt\"\n    else\n      MakeRelease \"md5-freebsd.txt\"\n    fi\n  elif [ \"$dockerType\" = \"web\" ]; then\n    echo \"web only\"\n  else\n    BuildRelease\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-lite.txt\"\n    else\n      MakeRelease \"md5.txt\"\n    fi\n  fi\nelif [ \"$buildType\" = \"prepare\" ]; then\n  if [ \"$dockerType\" = \"docker-multiplatform\" ]; then\n    PrepareBuildDockerMusl\n  fi\nelif [ \"$buildType\" = \"zip\" ]; then\n  if [ -n \"$otherParam\" ]; then\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"$otherParam-lite.txt\"\n    else\n      MakeRelease \"$otherParam.txt\"\n    fi\n  elif [ -n \"$dockerType\" ]; then\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"$dockerType-lite.txt\"\n    else\n      MakeRelease \"$dockerType.txt\"\n    fi\n  else\n    if [ \"$useLite\" = true ]; then\n      MakeRelease \"md5-lite.txt\"\n    else\n      MakeRelease \"md5.txt\"\n    fi\n  fi\nelse\n  echo -e \"Parameter error\"\n  echo -e \"Usage: $0 {dev|beta|release|zip|prepare} [docker|docker-multiplatform|linux_musl_arm|linux_musl|android|freebsd|web] [lite] [other_params]\"\n  echo -e \"Examples:\"\n  echo -e \"  $0 dev\"\n  echo -e \"  $0 dev lite\"\n  echo -e \"  $0 dev docker\"\n  echo -e \"  $0 dev docker lite\"\n  echo -e \"  $0 release\"\n  echo -e \"  $0 release lite\"\n  echo -e \"  $0 release docker lite\"\n  echo -e \"  $0 release linux_musl\"\nfi\n"
  },
  {
    "path": "cmd/admin.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/spf13/cobra\"\n)\n\n// AdminCmd represents the password command\nvar AdminCmd = &cobra.Command{\n\tUse:     \"admin\",\n\tAliases: []string{\"password\"},\n\tShort:   \"Show admin user's info and some operations about admin user's password\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\tadmin, err := op.GetAdmin()\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed get admin user: %+v\", err)\n\t\t} else {\n\t\t\tutils.Log.Infof(\"get admin user from CLI\")\n\t\t\tfmt.Println(\"Admin user's username:\", admin.Username)\n\t\t\tfmt.Println(\"The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed\")\n\t\t\tfmt.Println(\"You can reset the password with a random string by running [openlist admin random]\")\n\t\t\tfmt.Println(\"You can also set a new password by running [openlist admin set NEW_PASSWORD]\")\n\t\t}\n\t},\n}\n\nvar RandomPasswordCmd = &cobra.Command{\n\tUse:   \"random\",\n\tShort: \"Reset admin user's password to a random string\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tutils.Log.Infof(\"reset admin user's password to a random string from CLI\")\n\t\tnewPwd := random.String(8)\n\t\tsetAdminPassword(newPwd)\n\t},\n}\n\nvar SetPasswordCmd = &cobra.Command{\n\tUse:   \"set\",\n\tShort: \"Set admin user's password\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif len(args) == 0 {\n\t\t\treturn fmt.Errorf(\"Please enter the new password\")\n\t\t}\n\t\tsetAdminPassword(args[0])\n\t\treturn nil\n\t},\n}\n\nvar ShowTokenCmd = &cobra.Command{\n\tUse:   \"token\",\n\tShort: \"Show admin token\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\ttoken := setting.GetStr(conf.Token)\n\t\tutils.Log.Infof(\"show admin token from CLI\")\n\t\tfmt.Println(\"Admin token:\", token)\n\t},\n}\n\nfunc setAdminPassword(pwd string) {\n\tbootstrap.Init()\n\tdefer bootstrap.Release()\n\tadmin, err := op.GetAdmin()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed get admin user: %+v\", err)\n\t\treturn\n\t}\n\tadmin.SetPassword(pwd)\n\tif err := op.UpdateUser(admin); err != nil {\n\t\tutils.Log.Errorf(\"failed update admin user: %+v\", err)\n\t\treturn\n\t}\n\tutils.Log.Infof(\"admin user has been update from CLI\")\n\tfmt.Println(\"admin user has been updated:\")\n\tfmt.Println(\"username:\", admin.Username)\n\tfmt.Println(\"password:\", pwd)\n\tDelAdminCacheOnline()\n}\n\nfunc init() {\n\tRootCmd.AddCommand(AdminCmd)\n\tAdminCmd.AddCommand(RandomPasswordCmd)\n\tAdminCmd.AddCommand(SetPasswordCmd)\n\tAdminCmd.AddCommand(ShowTokenCmd)\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// passwordCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// passwordCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/cancel2FA.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/spf13/cobra\"\n)\n\n// Cancel2FACmd represents the delete2fa command\nvar Cancel2FACmd = &cobra.Command{\n\tUse:   \"cancel2fa\",\n\tShort: \"Delete 2FA of admin user\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\tadmin, err := op.GetAdmin()\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to get admin user: %+v\", err)\n\t\t} else {\n\t\t\terr := op.Cancel2FAByUser(admin)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"failed to cancel 2FA: %+v\", err)\n\t\t\t} else {\n\t\t\t\tutils.Log.Infof(\"2FA is canceled from CLI\")\n\t\t\t\tfmt.Println(\"2FA canceled\")\n\t\t\t\tDelAdminCacheOnline()\n\t\t\t}\n\t\t}\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(Cancel2FACmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// cancel2FACmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// cancel2FACmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/common.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc Init() {\n\tbootstrap.Init()\n}\n\nfunc Release() {\n\tbootstrap.Release()\n}\n\nvar pid = -1\nvar pidFile string\n\nfunc initDaemon() {\n\tex, err := os.Executable()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\texPath := filepath.Dir(ex)\n\t_ = os.MkdirAll(filepath.Join(exPath, \"daemon\"), 0700)\n\tpidFile = filepath.Join(exPath, \"daemon/pid\")\n\tif utils.Exists(pidFile) {\n\t\tbytes, err := os.ReadFile(pidFile)\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"failed to read pid file\", err)\n\t\t}\n\t\tid, err := strconv.Atoi(string(bytes))\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"failed to parse pid data\", err)\n\t\t}\n\t\tpid = id\n\t}\n}\n"
  },
  {
    "path": "cmd/crypt.go",
    "content": "package cmd\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\trcCrypt \"github.com/rclone/rclone/backend/crypt\"\n\t\"github.com/rclone/rclone/fs/config/configmap\"\n\t\"github.com/rclone/rclone/fs/config/obscure\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// encryption and decryption command format for Crypt driver\n\ntype options struct {\n\top  string //decrypt or encrypt\n\tsrc string //source dir or file\n\tdst string //out destination\n\n\tpwd                string //de/encrypt password\n\tsalt               string\n\tfilenameEncryption string //reference drivers\\crypt\\meta.go Addtion\n\tdirnameEncryption  string\n\tfilenameEncode     string\n\tsuffix             string\n}\n\nvar opt options\n\n// CryptCmd represents the crypt command\nvar CryptCmd = &cobra.Command{\n\tUse:     \"crypt\",\n\tShort:   \"Encrypt or decrypt local file or dir\",\n\tExample: `openlist crypt  -s ./src/encrypt/ --op=de --pwd=123456 --salt=345678`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\topt.validate()\n\t\topt.cryptFileDir()\n\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(CryptCmd)\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// versionCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\tCryptCmd.Flags().StringVarP(&opt.src, \"src\", \"s\", \"\", \"src file or dir to encrypt/decrypt\")\n\tCryptCmd.Flags().StringVarP(&opt.dst, \"dst\", \"d\", \"\", \"dst dir to output,if not set,output to src dir\")\n\tCryptCmd.Flags().StringVar(&opt.op, \"op\", \"\", \"de or en which stands for decrypt or encrypt\")\n\n\tCryptCmd.Flags().StringVar(&opt.pwd, \"pwd\", \"\", \"password used to encrypt/decrypt,if not contain ___Obfuscated___ prefix,will be obfuscated before used\")\n\tCryptCmd.Flags().StringVar(&opt.salt, \"salt\", \"\", \"salt used to encrypt/decrypt,if not contain ___Obfuscated___ prefix,will be obfuscated before used\")\n\tCryptCmd.Flags().StringVar(&opt.filenameEncryption, \"filename-encrypt\", \"off\", \"filename encryption mode: off,standard,obfuscate\")\n\tCryptCmd.Flags().StringVar(&opt.dirnameEncryption, \"dirname-encrypt\", \"false\", \"is dirname encryption enabled:true,false\")\n\tCryptCmd.Flags().StringVar(&opt.filenameEncode, \"filename-encode\", \"base64\", \"filename encoding mode: base64,base32,base32768\")\n\tCryptCmd.Flags().StringVar(&opt.suffix, \"suffix\", \".bin\", \"suffix for encrypted file,default is .bin\")\n}\n\nfunc (o *options) validate() {\n\tif o.src == \"\" {\n\t\tlog.Fatal(\"src can not be empty\")\n\t}\n\tif o.op != \"encrypt\" && o.op != \"decrypt\" && o.op != \"en\" && o.op != \"de\" {\n\t\tlog.Fatal(\"op must be encrypt or decrypt\")\n\t}\n\tif o.filenameEncryption != \"off\" && o.filenameEncryption != \"standard\" && o.filenameEncryption != \"obfuscate\" {\n\t\tlog.Fatal(\"filename_encryption must be off,standard,obfuscate\")\n\t}\n\tif o.filenameEncode != \"base64\" && o.filenameEncode != \"base32\" && o.filenameEncode != \"base32768\" {\n\t\tlog.Fatal(\"filename_encode must be base64,base32,base32768\")\n\t}\n\n}\n\nfunc (o *options) cryptFileDir() {\n\tsrc, _ := filepath.Abs(o.src)\n\tlog.Infof(\"src abs is %v\", src)\n\n\tfileInfo, err := os.Stat(src)\n\tif err != nil {\n\t\tlog.Fatalf(\"reading file/dir %v failed,err:%v\", src, err)\n\n\t}\n\tpwd := updateObfusParm(o.pwd)\n\tsalt := updateObfusParm(o.salt)\n\n\t//create cipher\n\tconfig := configmap.Simple{\n\t\t\"password\":                  pwd,\n\t\t\"password2\":                 salt,\n\t\t\"filename_encryption\":       o.filenameEncryption,\n\t\t\"directory_name_encryption\": o.dirnameEncryption,\n\t\t\"filename_encoding\":         o.filenameEncode,\n\t\t\"suffix\":                    o.suffix,\n\t\t\"pass_bad_blocks\":           \"\",\n\t}\n\tlog.Infof(\"config:%v\", config)\n\tcipher, err := rcCrypt.NewCipher(config)\n\tif err != nil {\n\t\tlog.Fatalf(\"create cipher failed,err:%v\", err)\n\n\t}\n\tdst := \"\"\n\t//check and create dst dir\n\tif o.dst != \"\" {\n\t\tdst, _ = filepath.Abs(o.dst)\n\t\tcheckCreateDir(dst)\n\t}\n\n\t// src is file\n\tif !fileInfo.IsDir() { //file\n\t\tif dst == \"\" {\n\t\t\tdst = filepath.Dir(src)\n\t\t}\n\t\to.cryptFile(cipher, src, dst)\n\t\treturn\n\t}\n\n\t// src is dir\n\tif dst == \"\" {\n\t\t//if src is dir and not set dst dir ,create ${src}_crypt dir as dst dir\n\t\tdst = path.Join(filepath.Dir(src), fileInfo.Name()+\"_crypt\")\n\t}\n\tlog.Infof(\"dst : %v\", dst)\n\n\tdirnameMap := make(map[string]string)\n\tpathSeparator := string(os.PathSeparator)\n\n\tfilepath.Walk(src, func(p string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get file %v info failed, err:%v\", p, err)\n\t\t\treturn err\n\t\t}\n\t\tif p == src {\n\t\t\treturn nil\n\t\t}\n\t\tlog.Infof(\"current path %v\", p)\n\n\t\t// relative path\n\t\trp := strings.ReplaceAll(p, src, \"\")\n\t\tlog.Infof(\"relative path %v\", rp)\n\n\t\trpds := strings.Split(rp, pathSeparator)\n\n\t\tif info.IsDir() {\n\t\t\t// absolute dst dir for current path\n\t\t\tdd := \"\"\n\n\t\t\tif o.dirnameEncryption == \"true\" {\n\t\t\t\tif o.op == \"encrypt\" || o.op == \"en\" {\n\t\t\t\t\tfor i := range rpds {\n\t\t\t\t\t\toname := rpds[i]\n\t\t\t\t\t\tif _, ok := dirnameMap[rpds[i]]; ok {\n\t\t\t\t\t\t\trpds[i] = dirnameMap[rpds[i]]\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trpds[i] = cipher.EncryptDirName(rpds[i])\n\t\t\t\t\t\t\tdirnameMap[oname] = rpds[i]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdd = path.Join(dst, strings.Join(rpds, pathSeparator))\n\t\t\t\t} else {\n\t\t\t\t\tfor i := range rpds {\n\t\t\t\t\t\toname := rpds[i]\n\t\t\t\t\t\tif _, ok := dirnameMap[rpds[i]]; ok {\n\t\t\t\t\t\t\trpds[i] = dirnameMap[rpds[i]]\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdnn, err := cipher.DecryptDirName(rpds[i])\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tlog.Fatalf(\"decrypt dir name %v failed,err:%v\", rpds[i], err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\trpds[i] = dnn\n\t\t\t\t\t\t\tdirnameMap[oname] = dnn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t\tdd = path.Join(dst, strings.Join(rpds, pathSeparator))\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tdd = path.Join(dst, rp)\n\t\t\t}\n\n\t\t\tlog.Infof(\"create output dir %v\", dd)\n\t\t\tcheckCreateDir(dd)\n\t\t\treturn nil\n\t\t}\n\n\t\t// file dst dir\n\t\tfdd := dst\n\n\t\tif o.dirnameEncryption == \"true\" {\n\t\t\tfor i := range rpds {\n\t\t\t\tif i == len(rpds)-1 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfdd = path.Join(fdd, dirnameMap[rpds[i]])\n\t\t\t}\n\n\t\t} else {\n\t\t\tfdd = path.Join(fdd, strings.Join(rpds[:len(rpds)-1], pathSeparator))\n\t\t}\n\n\t\tlog.Infof(\"file output dir %v\", fdd)\n\t\to.cryptFile(cipher, p, fdd)\n\t\treturn nil\n\t})\n\n}\n\nfunc (o *options) cryptFile(cipher *rcCrypt.Cipher, src string, dst string) {\n\tfileInfo, err := os.Stat(src)\n\tif err != nil {\n\t\tlog.Fatalf(\"get file %v  info failed,err:%v\", src, err)\n\n\t}\n\tfd, err := os.OpenFile(src, os.O_RDWR, 0666)\n\tif err != nil {\n\t\tlog.Fatalf(\"open file %v failed,err:%v\", src, err)\n\n\t}\n\tdefer fd.Close()\n\n\tvar cryptSrcReader io.Reader\n\tvar outFile string\n\tif o.op == \"encrypt\" || o.op == \"en\" {\n\t\tfilename := fileInfo.Name()\n\t\tif o.filenameEncryption != \"off\" {\n\t\t\tfilename = cipher.EncryptFileName(fileInfo.Name())\n\t\t\tlog.Infof(\"encrypt file name %v to %v\", fileInfo.Name(), filename)\n\t\t} else {\n\t\t\tfilename = fileInfo.Name() + o.suffix\n\t\t}\n\t\tcryptSrcReader, err = cipher.EncryptData(fd)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"encrypt file %v failed,err:%v\", src, err)\n\n\t\t}\n\t\toutFile = path.Join(dst, filename)\n\t} else {\n\t\tfilename := fileInfo.Name()\n\t\tif o.filenameEncryption != \"off\" {\n\t\t\tfilename, err = cipher.DecryptFileName(filename)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"decrypt file name %v failed,err:%v\", src, err)\n\t\t\t}\n\t\t\tlog.Infof(\"decrypt file name %v to %v, \", fileInfo.Name(), filename)\n\t\t} else {\n\t\t\tfilename = strings.TrimSuffix(filename, o.suffix)\n\t\t}\n\n\t\tcryptSrcReader, err = cipher.DecryptData(fd)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"decrypt file %v failed,err:%v\", src, err)\n\n\t\t}\n\t\toutFile = path.Join(dst, filename)\n\t}\n\t//write new file\n\twr, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY, 0755)\n\tif err != nil {\n\t\tlog.Fatalf(\"create file %v failed,err:%v\", outFile, err)\n\n\t}\n\tdefer wr.Close()\n\n\t_, err = io.Copy(wr, cryptSrcReader)\n\tif err != nil {\n\t\tlog.Fatalf(\"write file %v failed,err:%v\", outFile, err)\n\t}\n\n}\n\n// check dir exist ,if not ,create\nfunc checkCreateDir(dir string) {\n\t_, err := os.Stat(dir)\n\n\tif os.IsNotExist(err) {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"create dir %v failed,err:%v\", dir, err)\n\t\t}\n\t\treturn\n\t} else if err != nil {\n\t\tlog.Fatalf(\"read dir %v err: %v\", dir, err)\n\t}\n\n}\n\nfunc updateObfusParm(str string) string {\n\tobfuscatedPrefix := \"___Obfuscated___\"\n\tif !strings.HasPrefix(str, obfuscatedPrefix) {\n\t\tstr, err := obscure.Obscure(str)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"update obfuscated parameter failed,err:%v\", str)\n\t\t}\n\t} else {\n\t\tstr, _ = strings.CutPrefix(str, obfuscatedPrefix)\n\t}\n\treturn str\n}\n"
  },
  {
    "path": "cmd/flags/config.go",
    "content": "package flags\n\nvar (\n\tDataDir     string\n\tConfigPath  string\n\tDebug       bool\n\tNoPrefix    bool\n\tDev         bool\n\tForceBinDir bool\n\tLogStd      bool\n)\n"
  },
  {
    "path": "cmd/kill.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// KillCmd represents the kill command\nvar KillCmd = &cobra.Command{\n\tUse:   \"kill\",\n\tShort: \"Force kill openlist server process by daemon/pid file\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tkill()\n\t},\n}\n\nfunc kill() {\n\tinitDaemon()\n\tif pid == -1 {\n\t\tlog.Info(\"Seems not have been started. Try use `openlist start` to start server.\")\n\t\treturn\n\t}\n\tprocess, err := os.FindProcess(pid)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to find process by pid: %d, reason: %v\", pid, process)\n\t\treturn\n\t}\n\terr = process.Kill()\n\tif err != nil {\n\t\tlog.Errorf(\"failed to kill process %d: %v\", pid, err)\n\t} else {\n\t\tlog.Info(\"killed process: \", pid)\n\t}\n\terr = os.Remove(pidFile)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to remove pid file\")\n\t}\n\tpid = -1\n}\n\nfunc init() {\n\tRootCmd.AddCommand(KillCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// stopCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// stopCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/lang.go",
    "content": "/*\nPackage cmd\nCopyright © 2022 Noah Hsu<i@nn.ci>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype KV[V any] map[string]V\n\ntype Drivers KV[KV[interface{}]]\n\nvar frontendPath string\n\nfunc firstUpper(s string) string {\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn strings.ToUpper(s[:1]) + s[1:]\n}\n\nfunc convert(s string) string {\n\tss := strings.Split(s, \"_\")\n\tans := strings.Join(ss, \" \")\n\treturn firstUpper(ans)\n}\n\nfunc writeFile(name string, data interface{}) {\n\tf, err := os.Open(fmt.Sprintf(\"%s/src/lang/en/%s.json\", frontendPath, name))\n\tif err != nil {\n\t\tlog.Errorf(\"failed to open %s.json: %+v\", name, err)\n\t\treturn\n\t}\n\tdefer f.Close()\n\tcontent, err := io.ReadAll(f)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to read %s.json: %+v\", name, err)\n\t\treturn\n\t}\n\toldData := make(map[string]interface{})\n\tnewData := make(map[string]interface{})\n\terr = utils.Json.Unmarshal(content, &oldData)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to unmarshal %s.json: %+v\", name, err)\n\t\treturn\n\t}\n\tcontent, err = utils.Json.Marshal(data)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to marshal json: %+v\", err)\n\t\treturn\n\t}\n\terr = utils.Json.Unmarshal(content, &newData)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to unmarshal json: %+v\", err)\n\t\treturn\n\t}\n\tif mergeJson(newData, oldData) {\n\t\tlog.Infof(\"%s.json no changed, skip\", name)\n\t} else {\n\t\tlog.Infof(\"%s.json changed, update file\", name)\n\t\t//log.Infof(\"old: %+v\\nnew:%+v\", oldData, data)\n\t\tutils.WriteJsonToFile(fmt.Sprintf(\"lang/%s.json\", name), oldData, true)\n\t}\n}\n\nfunc mergeJson(source, target map[string]interface{}) bool {\n\tequal := true\n\tfor k, v := range source {\n\t\ttgtV, tgtOk := target[k]\n\t\tif !tgtOk {\n\t\t\tequal = false\n\t\t\ttarget[k] = v\n\t\t} else {\n\t\t\tsrcMap, srcIsMap := v.(map[string]interface{})\n\t\t\ttgtMap, tgtIsMap := tgtV.(map[string]interface{})\n\t\t\tif srcIsMap && tgtIsMap {\n\t\t\t\tequal = mergeJson(srcMap, tgtMap) && equal\n\t\t\t}\n\t\t}\n\t}\n\treturn equal\n}\n\nfunc generateDriversJson() {\n\tdrivers := make(Drivers)\n\tdrivers[\"drivers\"] = make(KV[interface{}])\n\tdrivers[\"config\"] = make(KV[interface{}])\n\tdriverInfoMap := op.GetDriverInfoMap()\n\tfor k, v := range driverInfoMap {\n\t\tdrivers[\"drivers\"][k] = convert(k)\n\t\titems := make(KV[interface{}])\n\t\tconfig := map[string]string{}\n\t\tif v.Config.Alert != \"\" {\n\t\t\talert := strings.SplitN(v.Config.Alert, \"|\", 2)\n\t\t\tif len(alert) > 1 {\n\t\t\t\tconfig[\"alert\"] = alert[1]\n\t\t\t}\n\t\t}\n\t\tdrivers[\"config\"][k] = config\n\t\tfor i := range v.Additional {\n\t\t\titem := v.Additional[i]\n\t\t\titems[item.Name] = convert(item.Name)\n\t\t\tif item.Help != \"\" {\n\t\t\t\titems[fmt.Sprintf(\"%s-tips\", item.Name)] = item.Help\n\t\t\t}\n\t\t\tif item.Type == conf.TypeSelect && len(item.Options) > 0 {\n\t\t\t\toptions := make(KV[string])\n\t\t\t\t_options := strings.Split(item.Options, \",\")\n\t\t\t\tfor _, o := range _options {\n\t\t\t\t\toptions[o] = convert(o)\n\t\t\t\t}\n\t\t\t\titems[fmt.Sprintf(\"%ss\", item.Name)] = options\n\t\t\t}\n\t\t}\n\t\tdrivers[k] = items\n\t}\n\twriteFile(\"drivers\", drivers)\n}\n\nfunc generateSettingsJson() {\n\tsettings := data.InitialSettings()\n\tsettingsLang := make(KV[any])\n\tfor _, setting := range settings {\n\t\tsettingsLang[setting.Key] = convert(setting.Key)\n\t\tif setting.Help != \"\" {\n\t\t\tsettingsLang[fmt.Sprintf(\"%s-tips\", setting.Key)] = setting.Help\n\t\t}\n\t\tif setting.Type == conf.TypeSelect && len(setting.Options) > 0 {\n\t\t\toptions := make(KV[string])\n\t\t\t_options := strings.Split(setting.Options, \",\")\n\t\t\tfor _, o := range _options {\n\t\t\t\toptions[o] = convert(o)\n\t\t\t}\n\t\t\tsettingsLang[fmt.Sprintf(\"%ss\", setting.Key)] = options\n\t\t}\n\t}\n\twriteFile(\"settings\", settingsLang)\n\t//utils.WriteJsonToFile(\"lang/settings.json\", settingsLang)\n}\n\n// LangCmd represents the lang command\nvar LangCmd = &cobra.Command{\n\tUse:   \"lang\",\n\tShort: \"Generate language json file\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfrontendPath, _ = cmd.Flags().GetString(\"frontend-path\")\n\t\tbootstrap.InitConfig()\n\t\terr := os.MkdirAll(\"lang\", 0777)\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"failed create folder: %s\", err.Error())\n\t\t}\n\t\tgenerateDriversJson()\n\t\tgenerateSettingsJson()\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(LangCmd)\n\n\t// Add frontend-path flag\n\tLangCmd.Flags().String(\"frontend-path\", \"../OpenList-Frontend\", \"Path to the frontend project directory\")\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// langCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// langCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/restart.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// RestartCmd represents the restart command\nvar RestartCmd = &cobra.Command{\n\tUse:   \"restart\",\n\tShort: \"Restart openlist server by daemon/pid file\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tstop()\n\t\tstart()\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(RestartCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// restartCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// restartCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar RootCmd = &cobra.Command{\n\tUse:   \"openlist\",\n\tShort: \"A file list program that supports multiple storage.\",\n\tLong: `A file list program that supports multiple storage,\nbuilt with love by OpenListTeam.\nComplete documentation is available at https://doc.oplist.org/`,\n}\n\nfunc Execute() {\n\tif err := RootCmd.Execute(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\tRootCmd.PersistentFlags().StringVar(&flags.DataDir, \"data\", \"data\", \"data directory (relative paths are resolved against the current working directory)\")\n\tRootCmd.PersistentFlags().StringVar(&flags.ConfigPath, \"config\", \"\", \"path to config.json (relative to current working directory; defaults to [data directory]/config.json, where [data directory] is set by --data)\")\n\tRootCmd.PersistentFlags().BoolVar(&flags.Debug, \"debug\", false, \"start with debug mode\")\n\tRootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, \"no-prefix\", false, \"disable env prefix\")\n\tRootCmd.PersistentFlags().BoolVar(&flags.Dev, \"dev\", false, \"start with dev mode\")\n\tRootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, \"force-bin-dir\", false, \"Force to use the directory where the binary file is located as data directory\")\n\tRootCmd.PersistentFlags().BoolVar(&flags.LogStd, \"log-std\", false, \"Force to log to std\")\n}\n"
  },
  {
    "path": "cmd/server.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/spf13/cobra\"\n)\n\n// ServerCmd represents the server command\nvar ServerCmd = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Start the server at the specified address\",\n\tLong: `Start the server at the specified address\nthe address is defined in config file`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\tbootstrap.Start()\n\t\t// Wait for interrupt signal to gracefully shutdown the server with\n\t\t// a timeout of 1 second.\n\t\tquit := make(chan os.Signal, 1)\n\t\t// kill (no param) default send syscanll.SIGTERM\n\t\t// kill -2 is syscall.SIGINT\n\t\t// kill -9 is syscall. SIGKILL but can\"t be catch, so don't need add it\n\t\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n\t\t<-quit\n\t\tbootstrap.Shutdown(1 * time.Second)\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(ServerCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// serverCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// serverCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n\n// OutOpenListInit 暴露用于外部启动server的函数\nfunc OutOpenListInit() {\n\tvar (\n\t\tcmd  *cobra.Command\n\t\targs []string\n\t)\n\tServerCmd.Run(cmd, args)\n}\n"
  },
  {
    "path": "cmd/start.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// StartCmd represents the start command\nvar StartCmd = &cobra.Command{\n\tUse:   \"start\",\n\tShort: \"Silent start openlist server with `--force-bin-dir`\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tstart()\n\t},\n}\n\nfunc start() {\n\tinitDaemon()\n\tif pid != -1 {\n\t\t_, err := os.FindProcess(pid)\n\t\tif err == nil {\n\t\t\tlog.Info(\"openlist already started, pid \", pid)\n\t\t\treturn\n\t\t}\n\t}\n\targs := os.Args\n\targs[1] = \"server\"\n\targs = append(args, \"--force-bin-dir\")\n\tcmd := &exec.Cmd{\n\t\tPath: args[0],\n\t\tArgs: args,\n\t\tEnv:  os.Environ(),\n\t}\n\tstdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), \"start.log\"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)\n\tif err != nil {\n\t\tlog.Fatal(os.Getpid(), \": failed to open start log file:\", err)\n\t}\n\tcmd.Stderr = stdout\n\tcmd.Stdout = stdout\n\terr = cmd.Start()\n\tif err != nil {\n\t\tlog.Fatal(\"failed to start children process: \", err)\n\t}\n\tlog.Infof(\"success start pid: %d\", cmd.Process.Pid)\n\terr = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)\n\tif err != nil {\n\t\tlog.Warn(\"failed to record pid, you may not be able to stop the program with `./openlist stop`\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(StartCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// startCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// startCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/stop_default.go",
    "content": "//go:build !windows\n\npackage cmd\n\nimport (\n\t\"os\"\n\t\"syscall\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// StopCmd represents the stop command\nvar StopCmd = &cobra.Command{\n\tUse:   \"stop\",\n\tShort: \"Stop openlist server by daemon/pid file\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tstop()\n\t},\n}\n\nfunc stop() {\n\tinitDaemon()\n\tif pid == -1 {\n\t\tlog.Info(\"Seems not have been started. Try use `openlist start` to start server.\")\n\t\treturn\n\t}\n\tprocess, err := os.FindProcess(pid)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to find process by pid: %d, reason: %v\", pid, process)\n\t\treturn\n\t}\n\terr = process.Signal(syscall.SIGTERM)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to terminate process %d: %v\", pid, err)\n\t} else {\n\t\tlog.Info(\"terminated process: \", pid)\n\t}\n\terr = os.Remove(pidFile)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to remove pid file\")\n\t}\n\tpid = -1\n}\n\nfunc init() {\n\tRootCmd.AddCommand(StopCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// stopCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// stopCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/stop_windows.go",
    "content": "//go:build windows\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// StopCmd represents the stop command\nvar StopCmd = &cobra.Command{\n\tUse:   \"stop\",\n\tShort: \"Same as the kill command\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tstop()\n\t},\n}\n\nfunc stop() {\n\tkill()\n}\n\nfunc init() {\n\tRootCmd.AddCommand(StopCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// stopCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// stopCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/storage.go",
    "content": "/*\nCopyright © 2023 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/charmbracelet/bubbles/table\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/spf13/cobra\"\n)\n\n// storageCmd represents the storage command\nvar storageCmd = &cobra.Command{\n\tUse:   \"storage\",\n\tShort: \"Manage storage\",\n}\n\nvar disableStorageCmd = &cobra.Command{\n\tUse:   \"disable [mount path]\",\n\tShort: \"Disable a storage by mount path\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"mount path is required\")\n\t\t}\n\t\tmountPath := args[0]\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\tstorage, err := db.GetStorageByMountPath(mountPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to query storage: %+v\", err)\n\t\t}\n\t\tstorage.Disabled = true\n\t\terr = db.UpdateStorage(storage)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update storage: %+v\", err)\n\t\t}\n\t\tutils.Log.Infof(\"Storage with mount path [%s] has been disabled from CLI\", mountPath)\n\t\tfmt.Printf(\"Storage with mount path [%s] has been disabled\\n\", mountPath)\n\t\treturn nil\n\t},\n}\n\nvar deleteStorageCmd = &cobra.Command{\n\tUse:   \"delete [id]\",\n\tShort: \"Delete a storage by id\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"id is required\")\n\t\t}\n\t\tid, err := strconv.Atoi(args[0])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"id must be a number\")\n\t\t}\n\n\t\tif force, _ := cmd.Flags().GetBool(\"force\"); force {\n\t\t\tfmt.Printf(\"Are you sure you want to delete storage with id [%d]? [y/N]: \", id)\n\t\t\tvar confirm string\n\t\t\tfmt.Scanln(&confirm)\n\t\t\tif confirm != \"y\" && confirm != \"Y\" {\n\t\t\t\tfmt.Println(\"Delete operation cancelled.\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\terr = db.DeleteStorageById(uint(id))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete storage by id: %+v\", err)\n\t\t}\n\t\tutils.Log.Infof(\"Storage with id [%d] have been deleted from CLI\", id)\n\t\tfmt.Printf(\"Storage with id [%d] have been deleted\\n\", id)\n\t\treturn nil\n\t},\n}\n\nvar baseStyle = lipgloss.NewStyle().\n\tBorderStyle(lipgloss.NormalBorder()).\n\tBorderForeground(lipgloss.Color(\"240\"))\n\ntype model struct {\n\ttable table.Model\n}\n\nfunc (m model) Init() tea.Cmd { return nil }\n\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"esc\":\n\t\t\tif m.table.Focused() {\n\t\t\t\tm.table.Blur()\n\t\t\t} else {\n\t\t\t\tm.table.Focus()\n\t\t\t}\n\t\tcase \"q\", \"ctrl+c\":\n\t\t\treturn m, tea.Quit\n\t\t\t//case \"enter\":\n\t\t\t//\treturn m, tea.Batch(\n\t\t\t//\t\ttea.Printf(\"Let's go to %s!\", m.table.SelectedRow()[1]),\n\t\t\t//\t)\n\t\t}\n\t}\n\tm.table, cmd = m.table.Update(msg)\n\treturn m, cmd\n}\n\nfunc (m model) View() string {\n\treturn baseStyle.Render(m.table.View()) + \"\\n\"\n}\n\nvar storageTableHeight int\nvar listStorageCmd = &cobra.Command{\n\tUse:   \"list\",\n\tShort: \"List all storages\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tbootstrap.Init()\n\t\tdefer bootstrap.Release()\n\t\tstorages, _, err := db.GetStorages(1, -1)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to query storages: %+v\", err)\n\t\t} else {\n\t\t\tfmt.Printf(\"Found %d storages\\n\", len(storages))\n\t\t\tcolumns := []table.Column{\n\t\t\t\t{Title: \"ID\", Width: 4},\n\t\t\t\t{Title: \"Driver\", Width: 16},\n\t\t\t\t{Title: \"Mount Path\", Width: 30},\n\t\t\t\t{Title: \"Enabled\", Width: 7},\n\t\t\t}\n\n\t\t\tvar rows []table.Row\n\t\t\tfor i := range storages {\n\t\t\t\tstorage := storages[i]\n\t\t\t\tenabled := \"true\"\n\t\t\t\tif storage.Disabled {\n\t\t\t\t\tenabled = \"false\"\n\t\t\t\t}\n\t\t\t\trows = append(rows, table.Row{\n\t\t\t\t\tstrconv.Itoa(int(storage.ID)),\n\t\t\t\t\tstorage.Driver,\n\t\t\t\t\tstorage.MountPath,\n\t\t\t\t\tenabled,\n\t\t\t\t})\n\t\t\t}\n\t\t\tt := table.New(\n\t\t\t\ttable.WithColumns(columns),\n\t\t\t\ttable.WithRows(rows),\n\t\t\t\ttable.WithFocused(true),\n\t\t\t\ttable.WithHeight(storageTableHeight),\n\t\t\t)\n\n\t\t\ts := table.DefaultStyles()\n\t\t\ts.Header = s.Header.\n\t\t\t\tBorderStyle(lipgloss.NormalBorder()).\n\t\t\t\tBorderForeground(lipgloss.Color(\"240\")).\n\t\t\t\tBorderBottom(true).\n\t\t\t\tBold(false)\n\t\t\ts.Selected = s.Selected.\n\t\t\t\tForeground(lipgloss.Color(\"229\")).\n\t\t\t\tBackground(lipgloss.Color(\"57\")).\n\t\t\t\tBold(false)\n\t\t\tt.SetStyles(s)\n\n\t\t\tm := model{t}\n\t\t\tif _, err := tea.NewProgram(m).Run(); err != nil {\n\t\t\t\tfmt.Printf(\"failed to run program: %+v\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\n\tRootCmd.AddCommand(storageCmd)\n\tstorageCmd.AddCommand(disableStorageCmd)\n\tstorageCmd.AddCommand(listStorageCmd)\n\tstorageCmd.PersistentFlags().IntVarP(&storageTableHeight, \"height\", \"H\", 10, \"Table height\")\n\tstorageCmd.AddCommand(deleteStorageCmd)\n\tdeleteStorageCmd.Flags().BoolP(\"force\", \"f\", false, \"Force delete without confirmation\")\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// storageCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// storageCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "cmd/user.go",
    "content": "package cmd\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc DelAdminCacheOnline() {\n\tadmin, err := op.GetAdmin()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[del_admin_cache] get admin error: %+v\", err)\n\t\treturn\n\t}\n\tDelUserCacheOnline(admin.Username)\n}\n\nfunc DelUserCacheOnline(username string) {\n\tclient := resty.New().SetTimeout(1 * time.Second).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})\n\ttoken := setting.GetStr(conf.Token)\n\tport := conf.Conf.Scheme.HttpPort\n\tu := fmt.Sprintf(\"http://localhost:%d/api/admin/user/del_cache\", port)\n\tif port == -1 {\n\t\tif conf.Conf.Scheme.HttpsPort == -1 {\n\t\t\tutils.Log.Warnf(\"[del_user_cache] no open port\")\n\t\t\treturn\n\t\t}\n\t\tu = fmt.Sprintf(\"https://localhost:%d/api/admin/user/del_cache\", conf.Conf.Scheme.HttpsPort)\n\t}\n\tres, err := client.R().SetHeader(\"Authorization\", token).SetQueryParam(\"username\", username).Post(u)\n\tif err != nil {\n\t\tutils.Log.Warnf(\"[del_user_cache_online] failed: %+v\", err)\n\t\treturn\n\t}\n\tif res.StatusCode() != 200 {\n\t\tutils.Log.Warnf(\"[del_user_cache_online] failed: %+v\", res.String())\n\t\treturn\n\t}\n\tcode := utils.Json.Get(res.Body(), \"code\").ToInt()\n\tmsg := utils.Json.Get(res.Body(), \"message\").ToString()\n\tif code != 200 {\n\t\tutils.Log.Errorf(\"[del_user_cache_online] error: %s\", msg)\n\t\treturn\n\t}\n\tutils.Log.Debugf(\"[del_user_cache_online] del user [%s] cache success\", username)\n}\n"
  },
  {
    "path": "cmd/version.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/spf13/cobra\"\n)\n\n// VersionCmd represents the version command\nvar VersionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Show current version of OpenList\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tgoVersion := fmt.Sprintf(\"%s %s/%s\", runtime.Version(), runtime.GOOS, runtime.GOARCH)\n\n\t\tfmt.Printf(`Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion: %s\nWebVersion: %s\n`, conf.BuiltAt, goVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)\n\t\tos.Exit(0)\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(VersionCmd)\n\n\t// Here you will define your flags and configuration settings.\n\n\t// Cobra supports Persistent Flags which will work for this command\n\t// and all subcommands, e.g.:\n\t// versionCmd.PersistentFlags().String(\"foo\", \"\", \"A help for foo\")\n\n\t// Cobra supports local flags which will only run when this command\n\t// is called directly, e.g.:\n\t// versionCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  openlist:\n    restart: always\n    volumes:\n      - '/etc/openlist:/opt/openlist/data'\n    ports:\n      - '5244:5244'\n      - '5245:5245'\n    user: '0:0'\n    environment:\n      - UMASK=022\n      - TZ=Asia/Shanghai\n    container_name: openlist\n    image: 'openlistteam/openlist:latest'\n"
  },
  {
    "path": "drivers/115/appver.go",
    "content": "package _115\n\nimport (\n\t\"errors\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tmd5Salt = \"Qclm8MGWUv59TnrR0XPg\"\n\tappVer  = \"35.6.0.3\"\n)\n\nfunc (d *Pan115) getAppVersion() (string, error) {\n\tresult := VersionResp{}\n\tres, err := base.RestyClient.R().Get(driver115.ApiGetVersion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = utils.Json.Unmarshal(res.Body(), &result)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(result.Error) > 0 {\n\t\treturn \"\", errors.New(result.Error)\n\t}\n\treturn result.Data.Win.Version, nil\n}\n\nfunc (d *Pan115) getAppVer() string {\n\tver, err := d.getAppVersion()\n\tif err != nil {\n\t\tlog.Warnf(\"[115] get app version failed: %v\", err)\n\t\treturn appVer\n\t}\n\tif len(ver) > 0 {\n\t\treturn ver\n\t}\n\treturn appVer\n}\n\nfunc (d *Pan115) initAppVer() {\n\tappVer = d.getAppVer()\n\tlog.Debugf(\"use app version: %v\", appVer)\n}\n\ntype VersionResp struct {\n\tError string   `json:\"error,omitempty\"`\n\tData  Versions `json:\"data\"`\n}\n\ntype Versions struct {\n\tWin Version `json:\"win\"`\n}\n\ntype Version struct {\n\tVersion string `json:\"version_code\"`\n}\n"
  },
  {
    "path": "drivers/115/driver.go",
    "content": "package _115\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Pan115 struct {\n\tmodel.Storage\n\tAddition\n\tclient     *driver115.Pan115Client\n\tlimiter    *rate.Limiter\n\tappVerOnce sync.Once\n}\n\nfunc (d *Pan115) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Pan115) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Pan115) Init(ctx context.Context) error {\n\td.appVerOnce.Do(d.initAppVer)\n\tif d.LimitRate > 0 {\n\t\td.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)\n\t}\n\treturn d.login()\n}\n\nfunc (d *Pan115) WaitLimit(ctx context.Context) error {\n\tif d.limiter != nil {\n\t\treturn d.limiter.Wait(ctx)\n\t}\n\treturn nil\n}\n\nfunc (d *Pan115) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil && !errors.Is(err, driver115.ErrNotExist) {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {\n\t\treturn &src, nil\n\t})\n}\n\nfunc (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tuserAgent := args.Header.Get(\"User-Agent\")\n\tdownloadInfo, err := d.client.DownloadWithUA(file.(*FileObj).PickCode, userAgent)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := &model.Link{\n\t\tURL:    downloadInfo.Url.Url,\n\t\tHeader: downloadInfo.Header,\n\t}\n\treturn link, nil\n}\n\nfunc (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := driver115.MkdirResp{}\n\tform := map[string]string{\n\t\t\"pid\":   parentDir.GetID(),\n\t\t\"cname\": dirName,\n\t}\n\treq := d.client.NewRequest().\n\t\tSetFormData(form).\n\t\tSetResult(&result).\n\t\tForceContentType(\"application/json;charset=UTF-8\")\n\n\tresp, err := req.Post(driver115.ApiDirAdd)\n\n\terr = driver115.CheckErr(err, &result, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tf, err := d.getNewFile(result.FileID)\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\treturn f, nil\n}\n\nfunc (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {\n\t\treturn nil, err\n\t}\n\tf, err := d.getNewFile(srcObj.GetID())\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\treturn f, nil\n}\n\nfunc (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := d.client.Rename(srcObj.GetID(), newName); err != nil {\n\t\treturn nil, err\n\t}\n\tf, err := d.getNewFile((srcObj.GetID()))\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\treturn f, nil\n}\n\nfunc (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\treturn d.client.Copy(dstDir.GetID(), srcObj.GetID())\n}\n\nfunc (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\treturn d.client.Delete(obj.GetID())\n}\n\nfunc (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tfastInfo *driver115.UploadInitResp\n\t\tdirID    = dstDir.GetID()\n\t)\n\n\tif ok, err := d.client.UploadAvailable(); err != nil || !ok {\n\t\treturn nil, err\n\t}\n\tif stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {\n\t\treturn nil, driver115.ErrUploadTooLarge\n\t}\n\t//if digest, err = d.client.GetDigestResult(stream); err != nil {\n\t//\treturn err\n\t//}\n\n\tconst PreHashSize int64 = 128 * utils.KB\n\thashSize := PreHashSize\n\tif stream.GetSize() < PreHashSize {\n\t\thashSize = stream.GetSize()\n\t}\n\treader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpreHash, err := utils.HashReader(utils.SHA1, reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpreHash = strings.ToUpper(preHash)\n\tfullHash := stream.GetHash().GetHash(utils.SHA1)\n\tif len(fullHash) != utils.SHA1.Width {\n\t\t_, fullHash, err = streamPkg.CacheFullAndHash(stream, &up, utils.SHA1)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfullHash = strings.ToUpper(fullHash)\n\n\t// rapid-upload\n\t// note that 115 add timeout for rapid-upload,\n\t// and \"sig invalid\" err is thrown even when the hash is correct after timeout.\n\tif fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {\n\t\treturn nil, err\n\t}\n\tif matched, err := fastInfo.Ok(); err != nil {\n\t\treturn nil, err\n\t} else if matched {\n\t\tf, err := d.getNewFileByPickCode(fastInfo.PickCode)\n\t\tif err != nil {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn f, nil\n\t}\n\n\tvar uploadResult *UploadResult\n\t// 闪传失败，上传\n\tif stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB，改用普通模式上传\n\t\tif uploadResult, err = d.UploadByOSS(ctx, &fastInfo.UploadOSSParams, stream, dirID, up); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\t// 分片上传\n\t\tif uploadResult, err = d.UploadByMultipart(ctx, &fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID, up); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tfile, err := d.getNewFile(uploadResult.Data.FileID)\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\treturn file, nil\n}\n\nfunc (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {\n\tresp, err := d.client.ListOfflineTask(0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Tasks, nil\n}\n\nfunc (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {\n\treturn d.client.AddOfflineTaskURIs(uris, dstDir.GetID(), driver115.WithAppVer(appVer))\n}\n\nfunc (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {\n\treturn d.client.DeleteOfflineTasks(hashes, deleteFiles)\n}\n\nfunc (d *Pan115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tinfo, err := d.client.GetInfo()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: info.SpaceInfo.AllTotal.Size,\n\t\t\tUsedSpace:  info.SpaceInfo.AllUse.Size,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Pan115)(nil)\n"
  },
  {
    "path": "drivers/115/meta.go",
    "content": "package _115\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tCookie       string  `json:\"cookie\" type:\"text\" help:\"one of QR code token and cookie required\"`\n\tQRCodeToken  string  `json:\"qrcode_token\" type:\"text\" help:\"one of QR code token and cookie required\"`\n\tQRCodeSource string  `json:\"qrcode_source\" type:\"select\" options:\"web,android,ios,tv,alipaymini,wechatmini,qandroid\" default:\"linux\" help:\"select the QR code device, default linux\"`\n\tPageSize     int64   `json:\"page_size\" type:\"number\" default:\"1000\" help:\"list api per page size of 115 driver\"`\n\tLimitRate    float64 `json:\"limit_rate\" type:\"float\" default:\"2\" help:\"limit all api request rate ([limit]r/1s)\"`\n\tdriver.RootID\n}\n\nvar config = driver.Config{\n\tName:          \"115 Cloud\",\n\tDefaultRoot:   \"0\",\n\tLinkCacheMode: driver.LinkCacheUA,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Pan115{}\n\t})\n}\n"
  },
  {
    "path": "drivers/115/types.go",
    "content": "package _115\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/SheltonZhu/115driver/pkg/driver\"\n)\n\nvar _ model.Obj = (*FileObj)(nil)\n\ntype FileObj struct {\n\tdriver.File\n}\n\nfunc (f *FileObj) CreateTime() time.Time {\n\treturn f.File.CreateTime\n}\n\nfunc (f *FileObj) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.SHA1, f.Sha1)\n}\n\nfunc (f *FileObj) Thumb() string {\n\treturn f.ThumbURL\n}\n\ntype UploadResult struct {\n\tdriver.BasicResp\n\tData struct {\n\t\tPickCode string `json:\"pick_code\"`\n\t\tFileSize int    `json:\"file_size\"`\n\t\tFileID   string `json:\"file_id\"`\n\t\tThumbURL string `json:\"thumb_url\"`\n\t\tSha1     string `json:\"sha1\"`\n\t\tAid      int    `json:\"aid\"`\n\t\tFileName string `json:\"file_name\"`\n\t\tCid      string `json:\"cid\"`\n\t\tIsVideo  int    `json:\"is_video\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/115/util.go",
    "content": "package _115\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tnetutil \"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tcipher \"github.com/SheltonZhu/115driver/pkg/crypto/ec115\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\t\"github.com/pkg/errors\"\n)\n\n// var UserAgent = driver115.UA115Browser\nfunc (d *Pan115) login() error {\n\tvar err error\n\topts := []driver115.Option{\n\t\tdriver115.UA(d.getUA()),\n\t\tfunc(c *driver115.Pan115Client) {\n\t\t\tc.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})\n\t\t},\n\t}\n\td.client = driver115.New(opts...)\n\tcr := &driver115.Credential{}\n\tif d.QRCodeToken != \"\" {\n\t\ts := &driver115.QRCodeSession{\n\t\t\tUID: d.QRCodeToken,\n\t\t}\n\t\tif cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to login by qrcode\")\n\t\t}\n\t\td.Cookie = fmt.Sprintf(\"UID=%s;CID=%s;SEID=%s;KID=%s\", cr.UID, cr.CID, cr.SEID, cr.KID)\n\t\td.QRCodeToken = \"\"\n\t} else if d.Cookie != \"\" {\n\t\tif err = cr.FromCookie(d.Cookie); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to login by cookies\")\n\t\t}\n\t\td.client.ImportCredential(cr)\n\t} else {\n\t\treturn errors.New(\"missing cookie or qrcode account\")\n\t}\n\treturn d.client.LoginCheck()\n}\n\nfunc (d *Pan115) getFiles(fileId string) ([]FileObj, error) {\n\tres := make([]FileObj, 0)\n\tif d.PageSize <= 0 {\n\t\td.PageSize = driver115.FileListLimit\n\t}\n\tfiles, err := d.client.ListWithLimit(fileId, d.PageSize, driver115.WithMultiUrls())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, file := range *files {\n\t\tres = append(res, FileObj{file})\n\t}\n\treturn res, nil\n}\n\nfunc (d *Pan115) getNewFile(fileId string) (*FileObj, error) {\n\tfile, err := d.client.GetFile(fileId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &FileObj{*file}, nil\n}\n\nfunc (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {\n\tresult := driver115.GetFileInfoResponse{}\n\treq := d.client.NewRequest().\n\t\tSetQueryParam(\"pick_code\", pickCode).\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetResult(&result)\n\tresp, err := req.Get(driver115.ApiFileInfo)\n\tif err := driver115.CheckErr(err, &result, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif len(result.Files) == 0 {\n\t\treturn nil, errors.New(\"not get file info\")\n\t}\n\tfileInfo := result.Files[0]\n\n\tf := &FileObj{}\n\tf.From(fileInfo)\n\treturn f, nil\n}\n\nfunc (d *Pan115) getUA() string {\n\treturn fmt.Sprintf(\"Mozilla/5.0 115Browser/%s\", appVer)\n}\n\nfunc (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {\n\tuserID := strconv.FormatInt(c.client.UserID, 10)\n\tuserIDMd5 := md5.Sum([]byte(userID))\n\ttokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))\n\treturn hex.EncodeToString(tokenMd5[:])\n}\n\nfunc (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {\n\tvar (\n\t\tecdhCipher   *cipher.EcdhCipher\n\t\tencrypted    []byte\n\t\tdecrypted    []byte\n\t\tencodedToken string\n\t\terr          error\n\t\ttarget       = \"U_1_\" + dirID\n\t\tbodyBytes    []byte\n\t\tresult       = driver115.UploadInitResp{}\n\t\tfileSizeStr  = strconv.FormatInt(fileSize, 10)\n\t)\n\tif ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserID := strconv.FormatInt(d.client.UserID, 10)\n\tform := url.Values{}\n\tform.Set(\"appid\", \"0\")\n\tform.Set(\"appversion\", appVer)\n\tform.Set(\"userid\", userID)\n\tform.Set(\"filename\", fileName)\n\tform.Set(\"filesize\", fileSizeStr)\n\tform.Set(\"fileid\", fileID)\n\tform.Set(\"target\", target)\n\tform.Set(\"sig\", d.client.GenerateSignature(fileID, target))\n\n\tsignKey, signVal := \"\", \"\"\n\tfor retry := true; retry; {\n\t\tt := driver115.NowMilli()\n\n\t\tif encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tparams := map[string]string{\n\t\t\t\"k_ec\": encodedToken,\n\t\t}\n\n\t\tform.Set(\"t\", t.String())\n\t\tform.Set(\"token\", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))\n\t\tif signKey != \"\" && signVal != \"\" {\n\t\t\tform.Set(\"sign_key\", signKey)\n\t\t\tform.Set(\"sign_val\", signVal)\n\t\t}\n\t\tif encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treq := d.client.NewRequest().\n\t\t\tSetQueryParams(params).\n\t\t\tSetBody(encrypted).\n\t\t\tSetHeaderVerbatim(\"Content-Type\", \"application/x-www-form-urlencoded\").\n\t\t\tSetDoNotParseResponse(true)\n\t\tresp, err := req.Post(driver115.ApiUploadInit)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdata := resp.RawBody()\n\t\tdefer data.Close()\n\t\tif bodyBytes, err = io.ReadAll(data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif result.Status == 7 {\n\t\t\t// Update signKey & signVal\n\t\t\tsignKey = result.SignKey\n\t\t\tsignVal, err = UploadDigestRange(stream, result.SignCheck)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tretry = false\n\t\t}\n\t\tresult.SHA1 = fileID\n\t}\n\n\treturn &result, nil\n}\n\nfunc UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {\n\tvar start, end int64\n\tif _, err = fmt.Sscanf(rangeSpec, \"%d-%d\", &start, &end); err != nil {\n\t\treturn\n\t}\n\n\tlength := end - start + 1\n\treader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thashStr, err := utils.HashReader(utils.SHA1, reader)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresult = strings.ToUpper(hashStr)\n\treturn\n}\n\n// UploadByOSS use aliyun sdk to upload\nfunc (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSParams, s model.FileStreamer, dirID string, up driver.UpdateProgress) (*UploadResult, error) {\n\tossToken, err := c.client.GetOSSToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tossClient, err := netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbucket, err := ossClient.Bucket(params.Bucket)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar bodyBytes []byte\n\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\tif err = bucket.PutObject(params.Object, r, append(\n\t\tdriver115.OssOption(params, ossToken),\n\t\toss.CallbackResult(&bodyBytes),\n\t)...); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar uploadResult UploadResult\n\tif err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &uploadResult, uploadResult.Err(string(bodyBytes))\n}\n\n// UploadByMultipart upload by mutipart blocks\nfunc (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.UploadOSSParams, fileSize int64, s model.FileStreamer,\n\tdirID string, up driver.UpdateProgress, opts ...driver115.UploadMultipartOption,\n) (*UploadResult, error) {\n\tvar (\n\t\tchunks    []oss.FileChunk\n\t\tparts     []oss.UploadPart\n\t\timur      oss.InitiateMultipartUploadResult\n\t\tossClient *oss.Client\n\t\tbucket    *oss.Bucket\n\t\tossToken  *driver115.UploadOSSTokenResp\n\t\tbodyBytes []byte\n\t\terr       error\n\t)\n\n\ttmpF, err := s.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toptions := driver115.DefalutUploadMultipartOptions()\n\tif len(opts) > 0 {\n\t\tfor _, f := range opts {\n\t\t\tf(options)\n\t\t}\n\t}\n\t// oss 启用Sequential必须按顺序上传\n\toptions.ThreadsNum = 1\n\n\tif ossToken, err = d.client.GetOSSToken(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif ossClient, err = netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif bucket, err = ossClient.Bucket(params.Bucket); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// ossToken一小时后就会失效，所以每50分钟重新获取一次\n\tticker := time.NewTicker(options.TokenRefreshTime)\n\tdefer ticker.Stop()\n\t// 设置超时\n\ttimeout := time.NewTimer(options.Timeout)\n\n\tif chunks, err = SplitFile(fileSize); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif imur, err = bucket.InitiateMultipartUpload(params.Object,\n\t\toss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),\n\t\toss.UserAgentHeader(driver115.OSSUserAgent),\n\t\toss.EnableSha1(), oss.Sequential(),\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\twg := sync.WaitGroup{}\n\twg.Add(len(chunks))\n\n\tchunksCh := make(chan oss.FileChunk)\n\terrCh := make(chan error)\n\tUploadedPartsCh := make(chan oss.UploadPart)\n\tquit := make(chan struct{})\n\n\t// producer\n\tgo chunksProducer(chunksCh, chunks)\n\tgo func() {\n\t\twg.Wait()\n\t\tquit <- struct{}{}\n\t}()\n\n\tcompletedNum := atomic.Int32{}\n\t// consumers\n\tfor i := 0; i < options.ThreadsNum; i++ {\n\t\tgo func(threadId int) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"recovered in %v\", r)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tfor chunk := range chunksCh {\n\t\t\t\tvar part oss.UploadPart // 出现错误就继续尝试，共尝试3次\n\t\t\t\tfor retry := 0; retry < 3; retry++ {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase <-ticker.C:\n\t\t\t\t\t\tif ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken\n\t\t\t\t\t\t\terrCh <- errors.Wrap(err, \"刷新token时出现错误\")\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t\tbuf := make([]byte, chunk.Size)\n\t\t\t\t\tif _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif part, err = bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, bytes.NewReader(buf)),\n\t\t\t\t\t\tchunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- errors.Wrap(err, fmt.Sprintf(\"上传 %s 的第%d个分片时出现错误：%v\", s.GetName(), chunk.Number, err))\n\t\t\t\t} else {\n\t\t\t\t\tnum := completedNum.Add(1)\n\t\t\t\t\tup(float64(num) * 100.0 / float64(len(chunks)))\n\t\t\t\t}\n\t\t\t\tUploadedPartsCh <- part\n\t\t\t}\n\t\t}(i)\n\t}\n\n\tgo func() {\n\t\tfor part := range UploadedPartsCh {\n\t\t\tparts = append(parts, part)\n\t\t\twg.Done()\n\t\t}\n\t}()\nLOOP:\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\t// 到时重新获取ossToken\n\t\t\tif ossToken, err = d.client.GetOSSToken(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase <-quit:\n\t\t\tbreak LOOP\n\t\tcase <-errCh:\n\t\t\treturn nil, err\n\t\tcase <-timeout.C:\n\t\t\treturn nil, fmt.Errorf(\"time out\")\n\t\t}\n\t}\n\n\t// 不知道啥原因，oss那边分片上传不计算sha1，导致115服务器校验错误\n\t// params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, \"${sha1}\", params.SHA1)\n\tif _, err := bucket.CompleteMultipartUpload(imur, parts, append(\n\t\tdriver115.OssOption(params, ossToken),\n\t\toss.CallbackResult(&bodyBytes),\n\t)...); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar uploadResult UploadResult\n\tif err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &uploadResult, uploadResult.Err(string(bodyBytes))\n}\n\nfunc chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {\n\tfor _, chunk := range chunks {\n\t\tch <- chunk\n\t}\n}\n\nfunc SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {\n\tfor i := int64(1); i < 10; i++ {\n\t\tif fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片\n\t\t\tif chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\tif fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片\n\t\tif chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\t// 单个分片大小不能小于100KB\n\tif chunks[0].Size < 100*utils.KB {\n\t\tif chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// SplitFileByPartNum splits big file into parts by the num of parts.\n// Split the file with specified parts count, returns the split result when error is nil.\nfunc SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {\n\tif chunkNum <= 0 || chunkNum > 10000 {\n\t\treturn nil, errors.New(\"chunkNum invalid\")\n\t}\n\n\tif int64(chunkNum) > fileSize {\n\t\treturn nil, errors.New(\"oss: chunkNum invalid\")\n\t}\n\n\tvar chunks []oss.FileChunk\n\tchunk := oss.FileChunk{}\n\tchunkN := (int64)(chunkNum)\n\tfor i := int64(0); i < chunkN; i++ {\n\t\tchunk.Number = int(i + 1)\n\t\tchunk.Offset = i * (fileSize / chunkN)\n\t\tif i == chunkN-1 {\n\t\t\tchunk.Size = fileSize/chunkN + fileSize%chunkN\n\t\t} else {\n\t\t\tchunk.Size = fileSize / chunkN\n\t\t}\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\treturn chunks, nil\n}\n\n// SplitFileByPartSize splits big file into parts by the size of parts.\n// Splits the file by the part size. Returns the FileChunk when error is nil.\nfunc SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {\n\tif chunkSize <= 0 {\n\t\treturn nil, errors.New(\"chunkSize invalid\")\n\t}\n\n\tchunkN := fileSize / chunkSize\n\tif chunkN >= 10000 {\n\t\treturn nil, errors.New(\"Too many parts, please increase part size\")\n\t}\n\n\tvar chunks []oss.FileChunk\n\tchunk := oss.FileChunk{}\n\tfor i := int64(0); i < chunkN; i++ {\n\t\tchunk.Number = int(i + 1)\n\t\tchunk.Offset = i * chunkSize\n\t\tchunk.Size = chunkSize\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\tif fileSize%chunkSize > 0 {\n\t\tchunk.Number = len(chunks) + 1\n\t\tchunk.Offset = int64(len(chunks)) * chunkSize\n\t\tchunk.Size = fileSize % chunkSize\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\treturn chunks, nil\n}\n"
  },
  {
    "path": "drivers/115_open/driver.go",
    "content": "package _115_open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tsdk \"github.com/OpenListTeam/115-sdk-go\"\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Open115 struct {\n\tmodel.Storage\n\tAddition\n\tclient  *sdk.Client\n\tlimiter *rate.Limiter\n}\n\nfunc (d *Open115) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Open115) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Open115) Init(ctx context.Context) error {\n\td.client = sdk.New(sdk.WithRefreshToken(d.Addition.RefreshToken),\n\t\tsdk.WithAccessToken(d.Addition.AccessToken),\n\t\tsdk.WithOnRefreshToken(func(s1, s2 string) {\n\t\t\td.Addition.AccessToken = s1\n\t\t\td.Addition.RefreshToken = s2\n\t\t\top.MustSaveDriverStorage(d)\n\t\t}))\n\tif flags.Debug || flags.Dev {\n\t\td.client.SetDebug(true)\n\t}\n\t_, err := d.client.UserInfo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif d.Addition.LimitRate > 0 {\n\t\td.limiter = rate.NewLimiter(rate.Limit(d.Addition.LimitRate), 1)\n\t}\n\tif d.PageSize <= 0 {\n\t\td.PageSize = 200\n\t} else if d.PageSize > 1150 {\n\t\td.PageSize = 1150\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open115) WaitLimit(ctx context.Context) error {\n\tif d.limiter != nil {\n\t\treturn d.limiter.Wait(ctx)\n\t}\n\treturn nil\n}\n\nfunc (d *Open115) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar res []model.Obj\n\tpageSize := int64(d.PageSize)\n\toffset := int64(0)\n\tfor {\n\t\tif err := d.WaitLimit(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp, err := d.client.GetFiles(ctx, &sdk.GetFilesReq{\n\t\t\tCID:    dir.GetID(),\n\t\t\tLimit:  pageSize,\n\t\t\tOffset: offset,\n\t\t\tASC:    d.Addition.OrderDirection == \"asc\",\n\t\t\tO:      d.Addition.OrderBy,\n\t\t\t// Cur:     1,\n\t\t\tShowDir: true,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, utils.MustSliceConvert(resp.Data, func(src sdk.GetFilesResp_File) model.Obj {\n\t\t\tobj := Obj(src)\n\t\t\treturn &obj\n\t\t})...)\n\t\tif len(res) >= int(resp.Count) {\n\t\t\tbreak\n\t\t}\n\t\toffset += pageSize\n\t}\n\treturn res, nil\n}\n\nfunc (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tvar ua string\n\tif args.Header != nil {\n\t\tua = args.Header.Get(\"User-Agent\")\n\t}\n\tif ua == \"\" {\n\t\tua = base.UserAgent\n\t}\n\tobj, ok := file.(*Obj)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"can't convert obj\")\n\t}\n\tpc := obj.Pc\n\tresp, err := d.client.DownURL(ctx, pc, ua)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu, ok := resp[obj.GetID()]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"can't get link\")\n\t}\n\treturn &model.Link{\n\t\tURL: u.URL.URL,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{ua},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := d.client.Mkdir(ctx, parentDir.GetID(), dirName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Obj{\n\t\tFid:  resp.FileID,\n\t\tPid:  parentDir.GetID(),\n\t\tFn:   dirName,\n\t\tFc:   \"0\",\n\t\tUpt:  time.Now().Unix(),\n\t\tUet:  time.Now().Unix(),\n\t\tUpPt: time.Now().Unix(),\n\t}, nil\n}\n\nfunc (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\t_, err := d.client.Move(ctx, &sdk.MoveReq{\n\t\tFileIDs: srcObj.GetID(),\n\t\tToCid:   dstDir.GetID(),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\t_, err := d.client.UpdateFile(ctx, &sdk.UpdateFileReq{\n\t\tFileID:  srcObj.GetID(),\n\t\tFileName: newName,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobj, ok := srcObj.(*Obj)\n\tif ok {\n\t\tobj.Fn = newName\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\t_, err := d.client.Copy(ctx, &sdk.CopyReq{\n\t\tPID:     dstDir.GetID(),\n\t\tFileID:  srcObj.GetID(),\n\t\tNoDupli: \"1\",\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *Open115) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\t_obj, ok := obj.(*Obj)\n\tif !ok {\n\t\treturn fmt.Errorf(\"can't convert obj\")\n\t}\n\t_, err := d.client.DelFile(ctx, &sdk.DelFileReq{\n\t\tFileIDs:  _obj.GetID(),\n\t\tParentID: _obj.Pid,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\terr := d.WaitLimit(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsha1 := file.GetHash().GetHash(utils.SHA1)\n\tif len(sha1) != utils.SHA1.Width {\n\t\t_, sha1, err = stream.CacheFullAndHash(file, &up, utils.SHA1)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tconst PreHashSize int64 = 128 * utils.KB\n\thashSize := PreHashSize\n\tif file.GetSize() < PreHashSize {\n\t\thashSize = file.GetSize()\n\t}\n\treader, err := file.RangeRead(http_range.Range{Start: 0, Length: hashSize})\n\tif err != nil {\n\t\treturn err\n\t}\n\tsha1128k, err := utils.HashReader(utils.SHA1, reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 1. Init\n\tresp, err := d.client.UploadInit(ctx, &sdk.UploadInitReq{\n\t\tFileName: file.GetName(),\n\t\tFileSize: file.GetSize(),\n\t\tTarget:   dstDir.GetID(),\n\t\tFileID:   strings.ToUpper(sha1),\n\t\tPreID:    strings.ToUpper(sha1128k),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Status == 2 {\n\t\tup(100)\n\t\treturn nil\n\t}\n\t// 2. two way verify\n\tif utils.SliceContains([]int{6, 7, 8}, resp.Status) {\n\t\tsignCheck := strings.Split(resp.SignCheck, \"-\") //\"sign_check\": \"2392148-2392298\" 取2392148-2392298之间的内容(包含2392148、2392298)的sha1\n\t\tstart, err := strconv.ParseInt(signCheck[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tend, err := strconv.ParseInt(signCheck[1], 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treader, err = file.RangeRead(http_range.Range{Start: start, Length: end - start + 1})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsignVal, err := utils.HashReader(utils.SHA1, reader)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tresp, err = d.client.UploadInit(ctx, &sdk.UploadInitReq{\n\t\t\tFileName: file.GetName(),\n\t\t\tFileSize: file.GetSize(),\n\t\t\tTarget:   dstDir.GetID(),\n\t\t\tFileID:   strings.ToUpper(sha1),\n\t\t\tPreID:    strings.ToUpper(sha1128k),\n\t\t\tSignKey:  resp.SignKey,\n\t\t\tSignVal:  strings.ToUpper(signVal),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.Status == 2 {\n\t\t\tup(100)\n\t\t\treturn nil\n\t\t}\n\t}\n\t// 3. get upload token\n\ttokenResp, err := d.client.UploadGetToken(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 4. upload\n\terr = d.multpartUpload(ctx, file, up, tokenResp, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Open115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {\n\treturn d.client.AddOfflineTaskURIs(ctx, uris, dstDir.GetID())\n}\n\nfunc (d *Open115) DeleteOfflineTask(ctx context.Context, infoHash string, deleteFiles bool) error {\n\treturn d.client.DeleteOfflineTask(ctx, infoHash, deleteFiles)\n}\n\nfunc (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, error) {\n\tresp, err := d.client.OfflineTaskList(ctx, 1)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (d *Open115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tuserInfo, err := d.client.UserInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := ParseInt64(userInfo.RtSpaceInfo.AllTotal.Size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := ParseInt64(userInfo.RtSpaceInfo.AllUse.Size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\n// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n// \t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n// \treturn nil, errs.NotImplement\n// }\n\n// func (d *Open115) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n// \t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n// \treturn nil, errs.NotImplement\n// }\n\n// func (d *Open115) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n// \t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n// \treturn nil, errs.NotImplement\n// }\n\n// func (d *Open115) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n// \t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n// \t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n// \t// return errs.NotImplement to use an internal archive tool\n// \treturn nil, errs.NotImplement\n// }\n\n//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Open115)(nil)\n"
  },
  {
    "path": "drivers/115_open/meta.go",
    "content": "package _115_open\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootID\n\t// define other\n\tOrderBy        string  `json:\"order_by\" type:\"select\" options:\"file_name,file_size,user_utime,file_type\"`\n\tOrderDirection string  `json:\"order_direction\" type:\"select\" options:\"asc,desc\"`\n\tLimitRate      float64 `json:\"limit_rate\" type:\"float\" default:\"1\" help:\"limit all api request rate ([limit]r/1s)\"`\n\tPageSize       int64   `json:\"page_size\" type:\"number\" default:\"200\" help:\"list api per page size of 115open driver\"`\n\tAccessToken    string  `json:\"access_token\" required:\"true\"`\n\tRefreshToken   string  `json:\"refresh_token\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:          \"115 Open\",\n\tDefaultRoot:   \"0\",\n\tLinkCacheMode: driver.LinkCacheUA,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Open115{}\n\t})\n}\n"
  },
  {
    "path": "drivers/115_open/types.go",
    "content": "package _115_open\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tsdk \"github.com/OpenListTeam/115-sdk-go\"\n)\n\ntype Obj sdk.GetFilesResp_File\n\n// Thumb implements model.Thumb.\nfunc (o *Obj) Thumb() string {\n\treturn o.Thumbnail\n}\n\n// CreateTime implements model.Obj.\nfunc (o *Obj) CreateTime() time.Time {\n\treturn time.Unix(o.UpPt, 0)\n}\n\n// GetHash implements model.Obj.\nfunc (o *Obj) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.SHA1, o.Sha1)\n}\n\n// GetID implements model.Obj.\nfunc (o *Obj) GetID() string {\n\treturn o.Fid\n}\n\n// GetName implements model.Obj.\nfunc (o *Obj) GetName() string {\n\treturn o.Fn\n}\n\n// GetPath implements model.Obj.\nfunc (o *Obj) GetPath() string {\n\treturn \"\"\n}\n\n// GetSize implements model.Obj.\nfunc (o *Obj) GetSize() int64 {\n\treturn o.FS\n}\n\n// IsDir implements model.Obj.\nfunc (o *Obj) IsDir() bool {\n\treturn o.Fc == \"0\"\n}\n\n// ModTime implements model.Obj.\nfunc (o *Obj) ModTime() time.Time {\n\treturn time.Unix(o.Upt, 0)\n}\n\nvar _ model.Obj = (*Obj)(nil)\nvar _ model.Thumb = (*Obj)(nil)\n"
  },
  {
    "path": "drivers/115_open/upload.go",
    "content": "package _115_open\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"time\"\n\n\tsdk \"github.com/OpenListTeam/115-sdk-go\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tnetutil \"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\t\"github.com/avast/retry-go\"\n)\n\nfunc calPartSize(fileSize int64) int64 {\n\tvar partSize int64 = 20 * utils.MB\n\tif fileSize > partSize {\n\t\tif fileSize > 1*utils.TB { // file Size over 1TB\n\t\t\tpartSize = 5 * utils.GB // file part size 5GB\n\t\t} else if fileSize > 768*utils.GB { // over 768GB\n\t\t\tpartSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part\n\t\t} else if fileSize > 512*utils.GB { // over 512GB\n\t\t\tpartSize = 82463373 // ≈ 78.6432MB\n\t\t} else if fileSize > 384*utils.GB { // over 384GB\n\t\t\tpartSize = 54975582 // ≈ 52.4288MB\n\t\t} else if fileSize > 256*utils.GB { // over 256GB\n\t\t\tpartSize = 41231687 // ≈ 39.3216MB\n\t\t} else if fileSize > 128*utils.GB { // over 128GB\n\t\t\tpartSize = 27487791 // ≈ 26.2144MB\n\t\t}\n\t}\n\treturn partSize\n}\n\nfunc (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {\n\tossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))\n\tif err != nil {\n\t\treturn err\n\t}\n\tbucket, err := ossClient.Bucket(initResp.Bucket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = bucket.PutObject(initResp.Object, tempF,\n\t\toss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),\n\t\toss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),\n\t)\n\n\treturn err\n}\n\n// type CallbackResult struct {\n// \tState   bool   `json:\"state\"`\n// \tCode    int    `json:\"code\"`\n// \tMessage string `json:\"message\"`\n// \tData    struct {\n// \t\tPickCode string `json:\"pick_code\"`\n// \t\tFileName string `json:\"file_name\"`\n// \t\tFileSize int64  `json:\"file_size\"`\n// \t\tFileID   string `json:\"file_id\"`\n// \t\tThumbURL string `json:\"thumb_url\"`\n// \t\tSha1     string `json:\"sha1\"`\n// \t\tAid      int    `json:\"aid\"`\n// \t\tCid      string `json:\"cid\"`\n// \t} `json:\"data\"`\n// }\n\nfunc (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {\n\tossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))\n\tif err != nil {\n\t\treturn err\n\t}\n\tbucket, err := ossClient.Bucket(initResp.Bucket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\timur, err := bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfileSize := stream.GetSize()\n\tchunkSize := calPartSize(fileSize)\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(chunkSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpartNum := (stream.GetSize() + chunkSize - 1) / chunkSize\n\tparts := make([]oss.UploadPart, partNum)\n\toffset := int64(0)\n\tfor i := int64(1); i <= partNum; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\n\t\tpartSize := chunkSize\n\t\tif i == partNum {\n\t\t\tpartSize = fileSize - (i-1)*chunkSize\n\t\t}\n\t\trd, err := ss.GetSectionReader(offset, partSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(func() error {\n\t\t\trd.Seek(0, io.SeekStart)\n\t\t\tpart, err := bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, rd), partSize, int(i))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tparts[i-1] = part\n\t\t\treturn nil\n\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second))\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif i == partNum {\n\t\t\toffset = fileSize\n\t\t} else {\n\t\t\toffset += partSize\n\t\t}\n\t\tup(float64(offset) * 100 / float64(fileSize))\n\t}\n\n\t// callbackRespBytes := make([]byte, 1024)\n\t_, err = bucket.CompleteMultipartUpload(\n\t\timur,\n\t\tparts,\n\t\toss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),\n\t\toss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),\n\t\t// oss.CallbackResult(&callbackRespBytes),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/115_open/util.go",
    "content": "package _115_open\n\nimport \"encoding/json\"\n\nfunc ParseInt64(v json.Number) (int64, error) {\n\ti, err := v.Int64()\n\tif err == nil {\n\t\treturn i, nil\n\t}\n\tf, e1 := v.Float64()\n\tif e1 == nil {\n\t\treturn int64(f), nil\n\t}\n\treturn int64(0), err\n}\n"
  },
  {
    "path": "drivers/115_share/driver.go",
    "content": "package _115_share\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Pan115Share struct {\n\tmodel.Storage\n\tAddition\n\tclient  *driver115.Pan115Client\n\tlimiter *rate.Limiter\n}\n\nfunc (d *Pan115Share) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Pan115Share) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Pan115Share) Init(ctx context.Context) error {\n\tif d.LimitRate > 0 {\n\t\td.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)\n\t}\n\n\treturn d.login()\n}\n\nfunc (d *Pan115Share) WaitLimit(ctx context.Context) error {\n\tif d.limiter != nil {\n\t\treturn d.limiter.Wait(ctx)\n\t}\n\treturn nil\n}\n\nfunc (d *Pan115Share) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tvar ua string\n\t// TODO: will use user agent from header\n\t// if args.Header != nil {\n\t// \tua = args.Header.Get(\"User-Agent\")\n\t// }\n\tif ua == \"\" {\n\t\tua = base.UserAgentNT\n\t}\n\tfiles := make([]driver115.ShareFile, 0)\n\tfileResp, err := d.client.GetShareSnapWithUA(ua, d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles = append(files, fileResp.Data.List...)\n\ttotal := fileResp.Data.Count\n\tcount := len(fileResp.Data.List)\n\tfor total > count {\n\t\tfileResp, err := d.client.GetShareSnap(\n\t\t\td.ShareCode, d.ReceiveCode, dir.GetID(),\n\t\t\tdriver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfiles = append(files, fileResp.Data.List...)\n\t\tcount += len(fileResp.Data.List)\n\t}\n\n\treturn utils.SliceConvert(files, transFunc)\n}\n\nfunc (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tvar ua string\n\tif args.Header != nil {\n\t\tua = args.Header.Get(\"User-Agent\")\n\t}\n\tif ua == \"\" {\n\t\tua = base.UserAgent\n\t}\n\tdownloadInfo, err := d.client.DownloadByShareCodeWithUA(ua, d.ShareCode, d.ReceiveCode, file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{URL: downloadInfo.URL.URL}, nil\n}\n\nfunc (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\treturn errs.NotSupport\n}\n\nvar _ driver.Driver = (*Pan115Share)(nil)\n"
  },
  {
    "path": "drivers/115_share/meta.go",
    "content": "package _115_share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tCookie       string  `json:\"cookie\" type:\"text\" help:\"one of QR code token and cookie required\"`\n\tQRCodeToken  string  `json:\"qrcode_token\" type:\"text\" help:\"one of QR code token and cookie required\"`\n\tQRCodeSource string  `json:\"qrcode_source\" type:\"select\" options:\"web,android,ios,tv,alipaymini,wechatmini,qandroid\" default:\"linux\" help:\"select the QR code device, default linux\"`\n\tPageSize     int64   `json:\"page_size\" type:\"number\" default:\"1000\" help:\"list api per page size of 115 driver\"`\n\tLimitRate    float64 `json:\"limit_rate\" type:\"float\" default:\"2\" help:\"limit all api request rate (1r/[limit_rate]s)\"`\n\tShareCode    string  `json:\"share_code\" type:\"text\" required:\"true\" help:\"share code of 115 share link\"`\n\tReceiveCode  string  `json:\"receive_code\" type:\"text\" required:\"true\" help:\"receive code of 115 share link\"`\n\tdriver.RootID\n}\n\nvar config = driver.Config{\n\tName:        \"115 Share\",\n\tDefaultRoot: \"0\",\n\tNoUpload:    true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Pan115Share{}\n\t})\n}\n"
  },
  {
    "path": "drivers/115_share/utils.go",
    "content": "package _115_share\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/pkg/errors\"\n)\n\nvar _ model.Obj = (*FileObj)(nil)\n\ntype FileObj struct {\n\tSize     int64\n\tSha1     string\n\tUtm      time.Time\n\tFileName string\n\tisDir    bool\n\tFileID   string\n\tThumbURL string\n}\n\nfunc (f *FileObj) CreateTime() time.Time {\n\treturn f.Utm\n}\n\nfunc (f *FileObj) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.SHA1, f.Sha1)\n}\n\nfunc (f *FileObj) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f *FileObj) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f *FileObj) ModTime() time.Time {\n\treturn f.Utm\n}\n\nfunc (f *FileObj) IsDir() bool {\n\treturn f.isDir\n}\n\nfunc (f *FileObj) GetID() string {\n\treturn f.FileID\n}\n\nfunc (f *FileObj) GetPath() string {\n\treturn \"\"\n}\n\nfunc (f *FileObj) Thumb() string {\n\treturn f.ThumbURL\n}\n\nfunc transFunc(sf driver115.ShareFile) (model.Obj, error) {\n\ttimeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar (\n\t\tutm    = time.Unix(timeInt, 0)\n\t\tisDir  = (sf.IsFile == 0)\n\t\tfileID = string(sf.FileID)\n\t)\n\tif isDir {\n\t\tfileID = string(sf.CategoryID)\n\t}\n\treturn &FileObj{\n\t\tSize:     int64(sf.Size),\n\t\tSha1:     sf.Sha1,\n\t\tUtm:      utm,\n\t\tFileName: string(sf.FileName),\n\t\tisDir:    isDir,\n\t\tFileID:   fileID,\n\t\tThumbURL: sf.ThumbURL,\n\t}, nil\n}\n\nfunc (d *Pan115Share) login() error {\n\tvar err error\n\topts := []driver115.Option{\n\t\tdriver115.UA(base.UserAgentNT),\n\t}\n\td.client = driver115.New(opts...)\n\tif _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, \"\"); err != nil {\n\t\treturn errors.Wrap(err, \"failed to get share snap\")\n\t}\n\tcr := &driver115.Credential{}\n\tif d.QRCodeToken != \"\" {\n\t\ts := &driver115.QRCodeSession{\n\t\t\tUID: d.QRCodeToken,\n\t\t}\n\t\tif cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to login by qrcode\")\n\t\t}\n\t\td.Cookie = fmt.Sprintf(\"UID=%s;CID=%s;SEID=%s;KID=%s\", cr.UID, cr.CID, cr.SEID, cr.KID)\n\t\td.QRCodeToken = \"\"\n\t} else if d.Cookie != \"\" {\n\t\tif err = cr.FromCookie(d.Cookie); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to login by cookies\")\n\t\t}\n\t\td.client.ImportCredential(cr)\n\t} else {\n\t\treturn errors.New(\"missing cookie or qrcode account\")\n\t}\n\n\treturn d.client.LoginCheck()\n}\n"
  },
  {
    "path": "drivers/123/driver.go",
    "content": "package _123\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Pan123 struct {\n\tmodel.Storage\n\tAddition\n\tapiRateLimit sync.Map\n}\n\nfunc (d *Pan123) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Pan123) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Pan123) Init(ctx context.Context) error {\n\t_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetHeader(\"platform\", \"web\")\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) Drop(ctx context.Context) error {\n\t_, _ = d.Request(Logout, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{})\n\t}, nil)\n\treturn nil\n}\n\nfunc (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetID(), dir.GetName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn src, nil\n\t})\n}\n\nfunc (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif f, ok := file.(File); ok {\n\t\tdata := base.Json{\n\t\t\t\"driveId\":   0,\n\t\t\t\"etag\":      f.Etag,\n\t\t\t\"fileId\":    f.FileId,\n\t\t\t\"fileName\":  f.FileName,\n\t\t\t\"s3keyFlag\": f.S3KeyFlag,\n\t\t\t\"size\":      f.Size,\n\t\t\t\"type\":      f.Type,\n\t\t}\n\t\tresp, err := d.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(data)\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdownloadUrl := utils.Json.Get(resp, \"data\", \"DownloadUrl\").ToString()\n\t\tou, err := url.Parse(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tu_ := ou.String()\n\t\tnu := ou.Query().Get(\"params\")\n\t\tif nu != \"\" {\n\t\t\tdu, _ := base64.StdEncoding.DecodeString(nu)\n\t\t\tu, err := url.Parse(string(du))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tu_ = u.String()\n\t\t}\n\n\t\tlog.Debug(\"download url: \", u_)\n\t\tres, err := base.NoRedirectClient.R().SetHeader(\"Referer\", \"https://www.123pan.com/\").Get(u_)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debug(res.String())\n\t\tlink := model.Link{\n\t\t\tURL: u_,\n\t\t}\n\t\tlog.Debugln(\"res code: \", res.StatusCode())\n\t\tif res.StatusCode() == 302 {\n\t\t\tlink.URL = res.Header().Get(\"location\")\n\t\t} else if res.StatusCode() < 300 {\n\t\t\tlink.URL = utils.Json.Get(res.Body(), \"data\", \"redirect_url\").ToString()\n\t\t}\n\t\tlink.Header = http.Header{\n\t\t\t\"Referer\": []string{fmt.Sprintf(\"%s://%s/\", ou.Scheme, ou.Host)},\n\t\t}\n\t\treturn &link, nil\n\t} else {\n\t\treturn nil, fmt.Errorf(\"can't convert obj\")\n\t}\n}\n\nfunc (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdata := base.Json{\n\t\t\"driveId\":      0,\n\t\t\"etag\":         \"\",\n\t\t\"fileName\":     dirName,\n\t\t\"parentFileId\": parentDir.GetID(),\n\t\t\"size\":         0,\n\t\t\"type\":         1,\n\t}\n\t_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := base.Json{\n\t\t\"fileIdList\":   []base.Json{{\"FileId\": srcObj.GetID()}},\n\t\t\"parentFileId\": dstDir.GetID(),\n\t}\n\t_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tdata := base.Json{\n\t\t\"driveId\":  0,\n\t\t\"fileId\":   srcObj.GetID(),\n\t\t\"fileName\": newName,\n\t}\n\t_, err := d.Request(Rename, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {\n\tif f, ok := obj.(File); ok {\n\t\tdata := base.Json{\n\t\t\t\"driveId\":           0,\n\t\t\t\"operation\":         true,\n\t\t\t\"fileTrashInfoList\": []File{f},\n\t\t}\n\t\t_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(data)\n\t\t}, nil)\n\t\treturn err\n\t} else {\n\t\treturn fmt.Errorf(\"can't convert obj\")\n\t}\n}\n\nfunc (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tetag := file.GetHash().GetHash(utils.MD5)\n\tvar err error\n\tif len(etag) < utils.MD5.Width {\n\t\t_, etag, err = stream.CacheFullAndHash(file, &up, utils.MD5)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdata := base.Json{\n\t\t\"driveId\":      0,\n\t\t\"duplicate\":    2, // 2->覆盖 1->重命名 0->默认\n\t\t\"etag\":         strings.ToLower(etag),\n\t\t\"fileName\":     file.GetName(),\n\t\t\"parentFileId\": dstDir.GetID(),\n\t\t\"size\":         file.GetSize(),\n\t\t\"type\":         0,\n\t}\n\tvar resp UploadResp\n\tres, err := d.Request(UploadRequest, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugln(\"upload request res: \", string(res))\n\tif resp.Data.Reuse || resp.Data.Key == \"\" {\n\t\treturn nil\n\t}\n\tif resp.Data.AccessKeyId == \"\" || resp.Data.SecretAccessKey == \"\" || resp.Data.SessionToken == \"\" {\n\t\terr = d.newUpload(ctx, &resp, file, up)\n\t\treturn err\n\t} else {\n\t\tcfg := &aws.Config{\n\t\t\tCredentials:      credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),\n\t\t\tRegion:           aws.String(\"123pan\"),\n\t\t\tEndpoint:         aws.String(resp.Data.EndPoint),\n\t\t\tS3ForcePathStyle: aws.Bool(true),\n\t\t}\n\t\ts, err := session.NewSession(cfg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuploader := s3manager.NewUploader(s)\n\t\tif file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\t\tuploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)\n\t\t}\n\t\tinput := &s3manager.UploadInput{\n\t\t\tBucket: &resp.Data.Bucket,\n\t\t\tKey:    &resp.Data.Key,\n\t\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\t\tReader:         file,\n\t\t\t\tUpdateProgress: up,\n\t\t\t}),\n\t\t}\n\t\t_, err = uploader.UploadWithContext(ctx, input)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err = d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileId\": resp.Data.FileId,\n\t\t}).SetContext(ctx)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) APIRateLimit(ctx context.Context, api string) error {\n\tvalue, _ := d.apiRateLimit.LoadOrStore(api,\n\t\trate.NewLimiter(rate.Every(700*time.Millisecond), 1))\n\tlimiter := value.(*rate.Limiter)\n\n\treturn limiter.Wait(ctx)\n}\n\nfunc (d *Pan123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tuserInfo, err := d.getUserInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp,\n\t\t\tUsedSpace:  userInfo.Data.SpaceUsed,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Pan123)(nil)\n"
  },
  {
    "path": "drivers/123/meta.go",
    "content": "package _123\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tUsername string `json:\"username\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\tdriver.RootID\n\t//OrderBy        string `json:\"order_by\" type:\"select\" options:\"file_id,file_name,size,update_at\" default:\"file_name\"`\n\t//OrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tAccessToken  string\n\tUploadThread int    `json:\"UploadThread\" type:\"number\" default:\"3\" help:\"the threads of upload\"`\n\tPlatform     string `json:\"platform\" type:\"string\" default:\"web\" help:\"the platform header value, sent with API requests\"`\n}\n\nvar config = driver.Config{\n\tName:        \"123Pan\",\n\tDefaultRoot: \"0\",\n\tLocalSort:   true,\n\tPreferProxy: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\t// 新增默认选项 要在RegisterDriver初始化设置 才会对正在使用的用户生效\n\t\treturn &Pan123{\n\t\t\tAddition: Addition{\n\t\t\t\tUploadThread: 3,\n\t\t\t\tPlatform:     \"web\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/123/types.go",
    "content": "package _123\n\nimport (\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype File struct {\n\tFileName    string    `json:\"FileName\"`\n\tSize        int64     `json:\"Size\"`\n\tUpdateAt    time.Time `json:\"UpdateAt\"`\n\tFileId      int64     `json:\"FileId\"`\n\tType        int       `json:\"Type\"`\n\tEtag        string    `json:\"Etag\"`\n\tS3KeyFlag   string    `json:\"S3KeyFlag\"`\n\tDownloadUrl string    `json:\"DownloadUrl\"`\n}\n\nfunc (f File) CreateTime() time.Time {\n\treturn f.UpdateAt\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.MD5, f.Etag)\n}\n\nfunc (f File) GetPath() string {\n\treturn \"\"\n}\n\nfunc (f File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) ModTime() time.Time {\n\treturn f.UpdateAt\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == 1\n}\n\nfunc (f File) GetID() string {\n\treturn strconv.FormatInt(f.FileId, 10)\n}\n\nfunc (f File) Thumb() string {\n\tif f.DownloadUrl == \"\" {\n\t\treturn \"\"\n\t}\n\tdu, err := url.Parse(f.DownloadUrl)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdu.Path = strings.TrimSuffix(du.Path, \"_24_24\") + \"_70_70\"\n\tquery := du.Query()\n\tquery.Set(\"w\", \"70\")\n\tquery.Set(\"h\", \"70\")\n\tif !query.Has(\"type\") {\n\t\tquery.Set(\"type\", strings.TrimPrefix(path.Base(f.FileName), \".\"))\n\t}\n\tif !query.Has(\"trade_key\") {\n\t\tquery.Set(\"trade_key\", \"123pan-thumbnail\")\n\t}\n\tdu.RawQuery = query.Encode()\n\treturn du.String()\n}\n\nvar _ model.Obj = (*File)(nil)\nvar _ model.Thumb = (*File)(nil)\n\n//func (f File) Thumb() string {\n//\n//}\n//var _ model.Thumb = (*File)(nil)\n\ntype Files struct {\n\t//BaseResp\n\tData struct {\n\t\tNext     string `json:\"Next\"`\n\t\tTotal    int    `json:\"Total\"`\n\t\tInfoList []File `json:\"InfoList\"`\n\t} `json:\"data\"`\n}\n\n//type DownResp struct {\n//\t//BaseResp\n//\tData struct {\n//\t\tDownloadUrl string `json:\"DownloadUrl\"`\n//\t} `json:\"data\"`\n//}\n\ntype UploadResp struct {\n\t//BaseResp\n\tData struct {\n\t\tAccessKeyId     string `json:\"AccessKeyId\"`\n\t\tBucket          string `json:\"Bucket\"`\n\t\tKey             string `json:\"Key\"`\n\t\tSecretAccessKey string `json:\"SecretAccessKey\"`\n\t\tSessionToken    string `json:\"SessionToken\"`\n\t\tFileId          int64  `json:\"FileId\"`\n\t\tReuse           bool   `json:\"Reuse\"`\n\t\tEndPoint        string `json:\"EndPoint\"`\n\t\tStorageNode     string `json:\"StorageNode\"`\n\t\tUploadId        string `json:\"UploadId\"`\n\t} `json:\"data\"`\n}\n\ntype S3PreSignedURLs struct {\n\tData struct {\n\t\tPreSignedUrls map[string]string `json:\"presignedUrls\"`\n\t} `json:\"data\"`\n}\n\ntype UserInfoResp struct {\n\tData struct {\n\t\tUid            int64  `json:\"UID\"`\n\t\tNickname       string `json:\"Nickname\"`\n\t\tSpaceUsed      int64  `json:\"SpaceUsed\"`\n\t\tSpacePermanent int64  `json:\"SpacePermanent\"`\n\t\tSpaceTemp      int64  `json:\"SpaceTemp\"`\n\t\tFileCount      int    `json:\"FileCount\"`\n\t} `json:\"data\"`\n}\n\ntype offlineResolveResp struct {\n\tData struct {\n\t\tList []struct {\n\t\t\tResult  int    `json:\"result\"`\n\t\t\tID      int64  `json:\"id\"`\n\t\t\tErrCode int    `json:\"err_code\"`\n\t\t\tErrMsg  string `json:\"err_msg\"`\n\t\t\tFiles   []struct {\n\t\t\t\tID int64 `json:\"id\"`\n\t\t\t} `json:\"files\"`\n\t\t} `json:\"list\"`\n\t} `json:\"data\"`\n}\n\ntype offlineSubmitResp struct {\n\tData struct {\n\t\tTaskList []struct {\n\t\t\tTaskID int64 `json:\"task_id\"`\n\t\t\tResult int   `json:\"result\"`\n\t\t} `json:\"task_list\"`\n\t} `json:\"data\"`\n}\n\ntype offlineTaskListResp struct {\n\tData struct {\n\t\tHasRun bool          `json:\"has_run\"`\n\t\tList   []offlineTask `json:\"list\"`\n\t\tTotal  int           `json:\"total\"`\n\t} `json:\"data\"`\n}\n\ntype offlineTask struct {\n\tTaskID     int64   `json:\"task_id\"`\n\tName       string  `json:\"name\"`\n\tStatus     int     `json:\"status\"`\n\tSize       int64   `json:\"size\"`\n\tThirdTask  string  `json:\"third_task_id\"`\n\tDownloaded int64   `json:\"downloaded\"`\n\tProgress   float64 `json:\"progress\"`\n\tUploadIDR  int64   `json:\"upload_idr\"`\n\tUploadName string  `json:\"upload_name\"`\n\tType       string  `json:\"type\"`\n\tSpeed      int64   `json:\"speed\"`\n}\n"
  },
  {
    "path": "drivers/123/upload.go",
    "content": "package _123\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {\n\tdata := base.Json{\n\t\t\"bucket\":          upReq.Data.Bucket,\n\t\t\"key\":             upReq.Data.Key,\n\t\t\"partNumberEnd\":   end,\n\t\t\"partNumberStart\": start,\n\t\t\"uploadId\":        upReq.Data.UploadId,\n\t\t\"StorageNode\":     upReq.Data.StorageNode,\n\t}\n\tvar s3PreSignedUrls S3PreSignedURLs\n\t_, err := d.Request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetContext(ctx)\n\t}, &s3PreSignedUrls)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &s3PreSignedUrls, nil\n}\n\nfunc (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {\n\tdata := base.Json{\n\t\t\"StorageNode\":     upReq.Data.StorageNode,\n\t\t\"bucket\":          upReq.Data.Bucket,\n\t\t\"key\":             upReq.Data.Key,\n\t\t\"partNumberEnd\":   end,\n\t\t\"partNumberStart\": start,\n\t\t\"uploadId\":        upReq.Data.UploadId,\n\t}\n\tvar s3PreSignedUrls S3PreSignedURLs\n\t_, err := d.Request(S3Auth, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetContext(ctx)\n\t}, &s3PreSignedUrls)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &s3PreSignedUrls, nil\n}\n\nfunc (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {\n\tdata := base.Json{\n\t\t\"StorageNode\": upReq.Data.StorageNode,\n\t\t\"bucket\":      upReq.Data.Bucket,\n\t\t\"fileId\":      upReq.Data.FileId,\n\t\t\"fileSize\":    file.GetSize(),\n\t\t\"isMultipart\": isMultipart,\n\t\t\"key\":         upReq.Data.Key,\n\t\t\"uploadId\":    upReq.Data.UploadId,\n\t}\n\t_, err := d.Request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetContext(ctx)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, up driver.UpdateProgress) error {\n\t// fetch s3 pre signed urls\n\tsize := file.GetSize()\n\tchunkSize := int64(16 * utils.MB)\n\tchunkCount := 1\n\tif size > chunkSize {\n\t\tchunkCount = int((size + chunkSize - 1) / chunkSize)\n\t}\n\n\tss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlastChunkSize := size % chunkSize\n\tif lastChunkSize == 0 {\n\t\tlastChunkSize = chunkSize\n\t}\n\t// only 1 batch is allowed\n\tbatchSize := 1\n\tgetS3UploadUrl := d.getS3Auth\n\tif chunkCount > 1 {\n\t\tbatchSize = 10\n\t\tgetS3UploadUrl = d.getS3PreSignedUrls\n\t}\n\n\tthread := min(int(chunkCount), d.UploadThread)\n\tthreadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\tfor i := 1; i <= chunkCount; i += batchSize {\n\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\tbreak\n\t\t}\n\t\tstart := i\n\t\tend := min(i+batchSize, chunkCount+1)\n\t\ts3PreSignedUrls, err := getS3UploadUrl(uploadCtx, upReq, start, end)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// upload each chunk\n\t\tfor cur := start; cur < end; cur++ {\n\t\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\toffset := int64(cur-1) * chunkSize\n\t\t\tcurSize := chunkSize\n\t\t\tif cur == chunkCount {\n\t\t\t\tcurSize = lastChunkSize\n\t\t\t}\n\t\t\tvar reader io.ReadSeeker\n\t\t\tthreadG.GoWithLifecycle(errgroup.Lifecycle{\n\t\t\t\tBefore: func(ctx context.Context) (err error) {\n\t\t\t\t\treader, err = ss.GetSectionReader(offset, curSize)\n\t\t\t\t\treturn\n\t\t\t\t},\n\t\t\t\tDo: func(ctx context.Context) (err error) {\n\t\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\t\tuploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]\n\t\t\t\t\tif uploadUrl == \"\" {\n\t\t\t\t\t\treturn fmt.Errorf(\"upload url is empty, s3PreSignedUrls: %+v\", s3PreSignedUrls)\n\t\t\t\t\t}\n\t\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, reader))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\treq.ContentLength = curSize\n\t\t\t\t\t//req.Header.Set(\"Content-Length\", strconv.FormatInt(curSize, 10))\n\t\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer res.Body.Close()\n\t\t\t\t\tif res.StatusCode == http.StatusForbidden {\n\t\t\t\t\t\t_, err, _ = singleflight.AnyGroup.Do(fmt.Sprintf(\"Pan123.newUpload_%p\", threadG), func() (any, error) {\n\t\t\t\t\t\t\tnewS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ts3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls\n\t\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn fmt.Errorf(\"upload s3 chunk %d failed, status code: %d\", cur, res.StatusCode)\n\t\t\t\t\t}\n\t\t\t\t\tif res.StatusCode != http.StatusOK {\n\t\t\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn fmt.Errorf(\"upload s3 chunk %d failed, status code: %d, body: %s\", cur, res.StatusCode, body)\n\t\t\t\t\t}\n\t\t\t\t\tprogress := 100 * float64(threadG.Success()+1) / float64(chunkCount+1)\n\t\t\t\t\tup(progress)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tAfter: func(err error) {\n\t\t\t\t\tss.FreeSectionReader(reader)\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\tif err := threadG.Wait(); err != nil {\n\t\treturn err\n\t}\n\tdefer up(100)\n\t// complete s3 upload\n\treturn d.completeS3(ctx, upReq, file, chunkCount > 1)\n}\n"
  },
  {
    "path": "drivers/123/util.go",
    "content": "package _123\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nconst (\n\tApi              = \"https://www.123pan.com/api\"\n\tAApi             = \"https://www.123pan.com/a/api\"\n\tBApi             = \"https://www.123pan.com/b/api\"\n\tLoginApi         = \"https://login.123pan.com/api\"\n\tMainApi          = BApi\n\tSignIn           = LoginApi + \"/user/sign_in\"\n\tLogout           = MainApi + \"/user/logout\"\n\tUserInfo         = MainApi + \"/user/info\"\n\tFileList         = MainApi + \"/file/list/new\"\n\tDownloadInfo     = MainApi + \"/file/download_info\"\n\tMkdir            = MainApi + \"/file/upload_request\"\n\tMove             = MainApi + \"/file/mod_pid\"\n\tRename           = MainApi + \"/file/rename\"\n\tTrash            = MainApi + \"/file/trash\"\n\tUploadRequest    = MainApi + \"/file/upload_request\"\n\tUploadComplete   = MainApi + \"/file/upload_complete\"\n\tS3PreSignedUrls  = MainApi + \"/file/s3_repare_upload_parts_batch\"\n\tS3Auth           = MainApi + \"/file/s3_upload_object/auth\"\n\tUploadCompleteV2 = MainApi + \"/file/upload_complete/v2\"\n\tS3Complete       = MainApi + \"/file/s3_complete_multipart_upload\"\n\n\tOfflineResolve    = MainApi + \"/v2/offline_download/task/resolve\"\n\tOfflineSubmit     = MainApi + \"/v2/offline_download/task/submit\"\n\tOfflineTaskList   = MainApi + \"/offline_download/task/list\"\n\tOfflineTaskDelete = MainApi + \"/offline_download/task/delete\"\n\t// AuthKeySalt      = \"8-8D$sL8gPjom7bk#cY\"\n)\n\nvar ErrOfflineTaskNotFound = errors.New(\"offline task not found\")\n\nfunc signPath(path string, os string, version string) (k string, v string) {\n\ttable := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}\n\trandom := fmt.Sprintf(\"%.f\", math.Round(1e7*rand.Float64()))\n\tnow := time.Now().In(time.FixedZone(\"CST\", 8*3600))\n\ttimestamp := fmt.Sprint(now.Unix())\n\tnowStr := []byte(now.Format(\"200601021504\"))\n\tfor i := 0; i < len(nowStr); i++ {\n\t\tnowStr[i] = table[nowStr[i]-48]\n\t}\n\ttimeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))\n\tdata := strings.Join([]string{timestamp, random, path, os, version, timeSign}, \"|\")\n\tdataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))\n\treturn timeSign, strings.Join([]string{timestamp, random, dataSign}, \"-\")\n}\n\nfunc GetApi(rawUrl string) string {\n\tu, _ := url.Parse(rawUrl)\n\tquery := u.Query()\n\tquery.Add(signPath(u.Path, \"web\", \"3\"))\n\tu.RawQuery = query.Encode()\n\treturn u.String()\n}\n\n//func GetApi(url string) string {\n//\tvm := js.New()\n//\tvm.Set(\"url\", url[22:])\n//\tr, err := vm.RunString(`\n//\t(function(e){\n//        function A(t, e) {\n//            e = 1 < arguments.length && void 0 !== e ? e : 10;\n//            for (var n = function() {\n//                for (var t = [], e = 0; e < 256; e++) {\n//                    for (var n = e, r = 0; r < 8; r++)\n//                        n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;\n//                    t[e] = n\n//                }\n//                return t\n//            }(), r = function(t) {\n//                t = t.replace(/\\\\r\\\\n/g, \"\\\\n\");\n//                for (var e = \"\", n = 0; n < t.length; n++) {\n//                    var r = t.charCodeAt(n);\n//                    r < 128 ? e += String.fromCharCode(r) : e = 127 < r && r < 2048 ? (e += String.fromCharCode(r >> 6 | 192)) + String.fromCharCode(63 & r | 128) : (e = (e += String.fromCharCode(r >> 12 | 224)) + String.fromCharCode(r >> 6 & 63 | 128)) + String.fromCharCode(63 & r | 128)\n//                }\n//                return e\n//            }(t), a = -1, i = 0; i < r.length; i++)\n//                a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];\n//            return (a = (-1 ^ a) >>> 0).toString(e)\n//        }\n//\n//\t   function v(t) {\n//\t       return (v = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function(t) {\n//\t                   return typeof t\n//\t               }\n//\t               : function(t) {\n//\t                   return t && \"function\" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? \"symbol\" : typeof t\n//\t               }\n//\t       )(t)\n//\t   }\n//\n//\t\tfor (p in a = Math.round(1e7 * Math.random()),\n//\t\to = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),\n//\t\tm = [\"a\", \"d\", \"e\", \"f\", \"g\", \"h\", \"l\", \"m\", \"y\", \"i\", \"j\", \"n\", \"o\", \"p\", \"k\", \"q\", \"r\", \"s\", \"t\", \"u\", \"b\", \"c\", \"v\", \"w\", \"s\", \"z\"],\n//\t\tu = function(t, e, n) {\n//\t\t\tvar r;\n//\t\t\tn = 2 < arguments.length && void 0 !== n ? n : 8;\n//\t\t\treturn 0 === arguments.length ? null : (r = \"object\" === v(t) ? t : (10 === \"\".concat(t).length && (t = 1e3 * Number.parseInt(t)),\n//\t\t\tnew Date(t)),\n//\t\t\tt += 6e4 * new Date(t).getTimezoneOffset(),\n//\t\t\t{\n//\t\t\t\ty: (r = new Date(t + 36e5 * n)).getFullYear(),\n//\t\t\t\tm: r.getMonth() + 1 < 10 ? \"0\".concat(r.getMonth() + 1) : r.getMonth() + 1,\n//\t\t\t\td: r.getDate() < 10 ? \"0\".concat(r.getDate()) : r.getDate(),\n//\t\t\t\th: r.getHours() < 10 ? \"0\".concat(r.getHours()) : r.getHours(),\n//\t\t\t\tf: r.getMinutes() < 10 ? \"0\".concat(r.getMinutes()) : r.getMinutes()\n//\t\t\t})\n//\t\t}(o),\n//\t\th = u.y,\n//\t\tg = u.m,\n//\t\tl = u.d,\n//\t\tc = u.h,\n//\t\tu = u.f,\n//\t\td = [h, g, l, c, u].join(\"\"),\n//\t\tf = [],\n//\t\td)\n//\t\t\tf.push(m[Number(d[p])]);\n//\t\treturn h = A(f.join(\"\")),\n//\t\tg = A(\"\".concat(o, \"|\").concat(a, \"|\").concat(e, \"|\").concat(\"web\", \"|\").concat(\"3\", \"|\").concat(h)),\n//\t\t\"\".concat(h, \"=\").concat(o, \"-\").concat(a, \"-\").concat(g);\n//\t})(url)\n//\t   `)\n//\tif err != nil {\n//\t\tfmt.Println(err)\n//\t\treturn url\n//\t}\n//\tv, _ := r.Export().(string)\n//\treturn url + \"?\" + v\n//}\n\nfunc (d *Pan123) login() error {\n\tvar body base.Json\n\tif utils.IsEmailFormat(d.Username) {\n\t\tbody = base.Json{\n\t\t\t\"mail\":     d.Username,\n\t\t\t\"password\": d.Password,\n\t\t\t\"type\":     2,\n\t\t}\n\t} else {\n\t\tbody = base.Json{\n\t\t\t\"passport\": d.Username,\n\t\t\t\"password\": d.Password,\n\t\t\t\"remember\": true,\n\t\t}\n\t}\n\tres, err := base.RestyClient.R().\n\t\tSetHeaders(map[string]string{\n\t\t\t\"origin\":      \"https://www.123pan.com\",\n\t\t\t\"referer\":     \"https://www.123pan.com/\",\n\t\t\t\"user-agent\":  \"Dart/2.19(dart:io)-openlist\",\n\t\t\t\"platform\":    \"web\",\n\t\t\t\"app-version\": \"3\",\n\t\t\t//\"user-agent\":  base.UserAgent,\n\t\t}).\n\t\tSetBody(body).Post(SignIn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif utils.Json.Get(res.Body(), \"code\").ToInt() != 200 {\n\t\terr = fmt.Errorf(utils.Json.Get(res.Body(), \"message\").ToString())\n\t} else {\n\t\td.AccessToken = utils.Json.Get(res.Body(), \"data\", \"token\").ToString()\n\t}\n\treturn err\n}\n\n//func authKey(reqUrl string) (*string, error) {\n//\treqURL, err := url.Parse(reqUrl)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\n//\tnowUnix := time.Now().Unix()\n//\trandom := rand.Intn(0x989680)\n//\n//\tp4 := fmt.Sprintf(\"%d|%d|%s|%s|%s|%s\", nowUnix, random, reqURL.Path, \"web\", \"3\", AuthKeySalt)\n//\tauthKey := fmt.Sprintf(\"%d-%d-%x\", nowUnix, random, md5.Sum([]byte(p4)))\n//\treturn &authKey, nil\n//}\n\nfunc (d *Pan123) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tisRetry := false\ndo:\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"origin\":        \"https://www.123pan.com\",\n\t\t\"referer\":       \"https://www.123pan.com/\",\n\t\t\"authorization\": \"Bearer \" + d.AccessToken,\n\t\t\"user-agent\":    \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client\",\n\t\t\"platform\":      d.Platform,\n\t\t\"app-version\":   \"3\",\n\t\t//\"user-agent\":    base.UserAgent,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\t//authKey, err := authKey(url)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//req.SetQueryParam(\"auth-key\", *authKey)\n\tres, err := req.Execute(method, GetApi(url))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbody := res.Body()\n\tcode := utils.Json.Get(body, \"code\").ToInt()\n\tif code != 0 {\n\t\tif !isRetry && code == 401 {\n\t\t\terr := d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tisRetry = true\n\t\t\tgoto do\n\t\t}\n\t\treturn nil, errors.New(jsoniter.Get(body, \"message\").ToString())\n\t}\n\treturn body, nil\n}\n\nfunc (d *Pan123) OfflineDownload(ctx context.Context, uri string, dstDir model.Obj) (int64, error) {\n\tvar resolveResp offlineResolveResp\n\t_, err := d.Request(OfflineResolve, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"urls\": uri,\n\t\t})\n\t}, &resolveResp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(resolveResp.Data.List) == 0 {\n\t\treturn 0, fmt.Errorf(\"offline resolve failed: empty response\")\n\t}\n\tif resolveResp.Data.List[0].Result != 0 {\n\t\tmsg := resolveResp.Data.List[0].ErrMsg\n\t\tif msg == \"\" {\n\t\t\tmsg = \"offline resolve failed\"\n\t\t}\n\t\treturn 0, fmt.Errorf(\"%s\", msg)\n\t}\n\tresourceID := resolveResp.Data.List[0].ID\n\tif resourceID == 0 {\n\t\treturn 0, fmt.Errorf(\"offline resolve failed: empty resource id\")\n\t}\n\tselectFileIDs := make([]int64, 0, len(resolveResp.Data.List[0].Files))\n\tfor _, f := range resolveResp.Data.List[0].Files {\n\t\tif f.ID > 0 {\n\t\t\tselectFileIDs = append(selectFileIDs, f.ID)\n\t\t}\n\t}\n\tif len(selectFileIDs) == 0 {\n\t\treturn 0, fmt.Errorf(\"offline resolve failed: empty file list\")\n\t}\n\tuploadDir, err := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"invalid destination dir id: %s\", dstDir.GetID())\n\t}\n\n\tvar submitResp offlineSubmitResp\n\t_, err = d.Request(OfflineSubmit, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"resource_list\": []base.Json{\n\t\t\t\t{\n\t\t\t\t\t\"resource_id\":    resourceID,\n\t\t\t\t\t\"select_file_id\": selectFileIDs,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"upload_dir\": uploadDir,\n\t\t})\n\t}, &submitResp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(submitResp.Data.TaskList) == 0 {\n\t\treturn 0, fmt.Errorf(\"offline submit failed: empty task list\")\n\t}\n\tif submitResp.Data.TaskList[0].Result != 0 {\n\t\treturn 0, fmt.Errorf(\"offline submit failed\")\n\t}\n\tif submitResp.Data.TaskList[0].TaskID == 0 {\n\t\treturn 0, fmt.Errorf(\"offline submit failed: empty task id\")\n\t}\n\treturn submitResp.Data.TaskList[0].TaskID, nil\n}\n\nfunc (d *Pan123) GetOfflineTask(ctx context.Context, taskID int64) (*offlineTask, error) {\n\tif taskID == 0 {\n\t\treturn nil, fmt.Errorf(\"invalid task id\")\n\t}\n\tpage := 1\n\tpageSize := 100\n\tstatusArr := []int{0, 1, 2, 3}\n\tfor {\n\t\tvar listResp offlineTaskListResp\n\t\t_, err := d.Request(OfflineTaskList, http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\t\"current_page\": page,\n\t\t\t\t\"page_size\":    pageSize,\n\t\t\t\t\"status_arr\":   statusArr,\n\t\t\t})\n\t\t}, &listResp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := range listResp.Data.List {\n\t\t\tif listResp.Data.List[i].TaskID == taskID {\n\t\t\t\treturn &listResp.Data.List[i], nil\n\t\t\t}\n\t\t}\n\t\tif len(listResp.Data.List) == 0 || page*pageSize >= listResp.Data.Total {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\n\treturn nil, ErrOfflineTaskNotFound\n}\n\nfunc (d *Pan123) DeleteOfflineTasks(ctx context.Context, taskIDs []int64) error {\n\tif len(taskIDs) == 0 {\n\t\treturn nil\n\t}\n\t_, err := d.Request(OfflineTaskDelete, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"task_ids\": taskIDs,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {\n\tpage := 1\n\ttotal := 0\n\tres := make([]File, 0)\n\t// 2024-02-06 fix concurrency by 123pan\n\tfor {\n\t\tif err := d.APIRateLimit(ctx, FileList); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar resp Files\n\t\tquery := map[string]string{\n\t\t\t\"driveId\":              \"0\",\n\t\t\t\"limit\":                \"100\",\n\t\t\t\"next\":                 \"0\",\n\t\t\t\"orderBy\":              \"file_id\",\n\t\t\t\"orderDirection\":       \"desc\",\n\t\t\t\"parentFileId\":         parentId,\n\t\t\t\"trashed\":              \"false\",\n\t\t\t\"SearchData\":           \"\",\n\t\t\t\"Page\":                 strconv.Itoa(page),\n\t\t\t\"OnlyLookAbnormalFile\": \"0\",\n\t\t\t\"event\":                \"homeListFile\",\n\t\t\t\"operateType\":          \"4\",\n\t\t\t\"inDirectSpace\":        \"false\",\n\t\t}\n\t\t_res, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debug(string(_res))\n\t\tpage++\n\t\tres = append(res, resp.Data.InfoList...)\n\t\ttotal = resp.Data.Total\n\t\tif len(resp.Data.InfoList) == 0 || resp.Data.Next == \"-1\" {\n\t\t\tbreak\n\t\t}\n\t}\n\tif len(res) != total {\n\t\tlog.Warnf(\"incorrect file count from remote at %s: expected %d, got %d\", name, total, len(res))\n\t}\n\treturn res, nil\n}\n\nfunc (d *Pan123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {\n\tvar resp UserInfoResp\n\t_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/123_link/driver.go",
    "content": "package _123Link\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Pan123Link struct {\n\tmodel.Storage\n\tAddition\n\troot *Node\n}\n\nfunc (d *Pan123Link) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Pan123Link) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Pan123Link) Init(ctx context.Context) error {\n\tnode, err := BuildTree(d.OriginURLs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnode.calSize()\n\td.root = node\n\treturn nil\n}\n\nfunc (d *Pan123Link) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (Addition) GetRootPath() string {\n\treturn \"/\"\n}\n\nfunc (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {\n\tnode := GetNodeFromRootByPath(d.root, path)\n\treturn nodeToObj(node, path)\n}\n\nfunc (d *Pan123Link) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tnode := GetNodeFromRootByPath(d.root, dir.GetPath())\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\treturn utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {\n\t\treturn nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))\n\t})\n}\n\nfunc (d *Pan123Link) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tnode := GetNodeFromRootByPath(d.root, file.GetPath())\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\tsignUrl, err := SignURL(node.Url, d.PrivateKey, d.UID, time.Duration(d.ValidDuration)*time.Minute)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &model.Link{\n\t\t\tURL: signUrl,\n\t\t}, nil\n\t}\n\treturn nil, errs.NotFile\n}\n\nvar _ driver.Driver = (*Pan123Link)(nil)\n"
  },
  {
    "path": "drivers/123_link/meta.go",
    "content": "package _123Link\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tOriginURLs    string `json:\"origin_urls\" type:\"text\" required:\"true\" default:\"https://vip.123pan.com/29/folder/file.mp3\" help:\"structure:FolderName:\\n  [FileSize:][Modified:]Url\"`\n\tPrivateKey    string `json:\"private_key\"`\n\tUID           uint64 `json:\"uid\" type:\"number\"`\n\tValidDuration int64  `json:\"valid_duration\" type:\"number\" default:\"30\" help:\"minutes\"`\n}\n\nvar config = driver.Config{\n\tName: \"123PanLink\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Pan123Link{}\n\t})\n}\n"
  },
  {
    "path": "drivers/123_link/parse.go",
    "content": "package _123Link\n\nimport (\n\t\"fmt\"\n\turl2 \"net/url\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// build tree from text, text structure definition:\n/**\n * FolderName:\n *   [FileSize:][Modified:]Url\n */\n/**\n * For example:\n * folder1:\n *   name1:url1\n *   url2\n *   folder2:\n *     url3\n *     url4\n *   url5\n * folder3:\n *   url6\n *   url7\n * url8\n */\n// if there are no name, use the last segment of url as name\nfunc BuildTree(text string) (*Node, error) {\n\tlines := strings.Split(text, \"\\n\")\n\tvar root = &Node{Level: -1, Name: \"root\"}\n\tstack := []*Node{root}\n\tfor _, line := range lines {\n\t\t// calculate indent\n\t\tindent := 0\n\t\tfor i := 0; i < len(line); i++ {\n\t\t\tif line[i] != ' ' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tindent++\n\t\t}\n\t\t// if indent is not a multiple of 2, it is an error\n\t\tif indent%2 != 0 {\n\t\t\treturn nil, fmt.Errorf(\"the line '%s' is not a multiple of 2\", line)\n\t\t}\n\t\t// calculate level\n\t\tlevel := indent / 2\n\t\tline = strings.TrimSpace(line[indent:])\n\t\t// if the line is empty, skip\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// if level isn't greater than the level of the top of the stack\n\t\t// it is not the child of the top of the stack\n\t\tfor level <= stack[len(stack)-1].Level {\n\t\t\t// pop the top of the stack\n\t\t\tstack = stack[:len(stack)-1]\n\t\t}\n\t\t// if the line is a folder\n\t\tif isFolder(line) {\n\t\t\t// create a new node\n\t\t\tnode := &Node{\n\t\t\t\tLevel: level,\n\t\t\t\tName:  strings.TrimSuffix(line, \":\"),\n\t\t\t}\n\t\t\t// add the node to the top of the stack\n\t\t\tstack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)\n\t\t\t// push the node to the stack\n\t\t\tstack = append(stack, node)\n\t\t} else {\n\t\t\t// if the line is a file\n\t\t\t// create a new node\n\t\t\tnode, err := parseFileLine(line)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tnode.Level = level\n\t\t\t// add the node to the top of the stack\n\t\t\tstack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)\n\t\t}\n\t}\n\treturn root, nil\n}\n\nfunc isFolder(line string) bool {\n\treturn strings.HasSuffix(line, \":\")\n}\n\n// line definition:\n// [FileSize:][Modified:]Url\nfunc parseFileLine(line string) (*Node, error) {\n\t// if there is no url, it is an error\n\tif !strings.Contains(line, \"http://\") && !strings.Contains(line, \"https://\") {\n\t\treturn nil, fmt.Errorf(\"invalid line: %s, because url is required for file\", line)\n\t}\n\tindex := strings.Index(line, \"http://\")\n\tif index == -1 {\n\t\tindex = strings.Index(line, \"https://\")\n\t}\n\turl := line[index:]\n\tinfo := line[:index]\n\tnode := &Node{\n\t\tUrl: url,\n\t}\n\tname := stdpath.Base(url)\n\tunescape, err := url2.PathUnescape(name)\n\tif err == nil {\n\t\tname = unescape\n\t}\n\tnode.Name = name\n\tif index > 0 {\n\t\tif !strings.HasSuffix(info, \":\") {\n\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file info must end with ':'\", line)\n\t\t}\n\t\tinfo = info[:len(info)-1]\n\t\tif info == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file name can't be empty\", line)\n\t\t}\n\t\tinfoParts := strings.Split(info, \":\")\n\t\tsize, err := strconv.ParseInt(infoParts[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file size must be an integer\", line)\n\t\t}\n\t\tnode.Size = size\n\t\tif len(infoParts) > 1 {\n\t\t\tmodified, err := strconv.ParseInt(infoParts[1], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file modified must be an unix timestamp\", line)\n\t\t\t}\n\t\t\tnode.Modified = modified\n\t\t} else {\n\t\t\tnode.Modified = time.Now().Unix()\n\t\t}\n\t}\n\treturn node, nil\n}\n\nfunc splitPath(path string) []string {\n\tif path == \"/\" {\n\t\treturn []string{\"root\"}\n\t}\n\tparts := strings.Split(path, \"/\")\n\tparts[0] = \"root\"\n\treturn parts\n}\n\nfunc GetNodeFromRootByPath(root *Node, path string) *Node {\n\treturn root.getByPath(splitPath(path))\n}\n"
  },
  {
    "path": "drivers/123_link/types.go",
    "content": "package _123Link\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\n// Node is a node in the folder tree\ntype Node struct {\n\tUrl      string\n\tName     string\n\tLevel    int\n\tModified int64\n\tSize     int64\n\tChildren []*Node\n}\n\nfunc (node *Node) getByPath(paths []string) *Node {\n\tif len(paths) == 0 || node == nil {\n\t\treturn nil\n\t}\n\tif node.Name != paths[0] {\n\t\treturn nil\n\t}\n\tif len(paths) == 1 {\n\t\treturn node\n\t}\n\tfor _, child := range node.Children {\n\t\ttmp := child.getByPath(paths[1:])\n\t\tif tmp != nil {\n\t\t\treturn tmp\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (node *Node) isFile() bool {\n\treturn node.Url != \"\"\n}\n\nfunc (node *Node) calSize() int64 {\n\tif node.isFile() {\n\t\treturn node.Size\n\t}\n\tvar size int64 = 0\n\tfor _, child := range node.Children {\n\t\tsize += child.calSize()\n\t}\n\tnode.Size = size\n\treturn size\n}\n\nfunc nodeToObj(node *Node, path string) (model.Obj, error) {\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\treturn &model.Object{\n\t\tName:     node.Name,\n\t\tSize:     node.Size,\n\t\tModified: time.Unix(node.Modified, 0),\n\t\tIsFolder: !node.isFile(),\n\t\tPath:     path,\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/123_link/util.go",
    "content": "package _123Link\n\nimport (\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/url\"\n\t\"time\"\n)\n\nfunc SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {\n\tif privateKey == \"\" {\n\t\treturn originURL, nil\n\t}\n\tvar (\n\t\tts     = time.Now().Add(validDuration).Unix() // 有效时间戳\n\t\trInt   = rand.Int()                           // 随机正整数\n\t\tobjURL *url.URL\n\t)\n\tobjURL, err = url.Parse(originURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tauthKey := fmt.Sprintf(\"%d-%d-%d-%x\", ts, rInt, uid, md5.Sum([]byte(fmt.Sprintf(\"%s-%d-%d-%d-%s\",\n\t\tobjURL.Path, ts, rInt, uid, privateKey))))\n\tv := objURL.Query()\n\tv.Add(\"auth_key\", authKey)\n\tobjURL.RawQuery = v.Encode()\n\treturn objURL.String(), nil\n}\n"
  },
  {
    "path": "drivers/123_open/driver.go",
    "content": "package _123_open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Open123 struct {\n\tmodel.Storage\n\tAddition\n\tUID uint64\n\ttm  *tokenManager\n}\n\nfunc (d *Open123) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Open123) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Open123) Init(ctx context.Context) error {\n\tif d.UploadThread < 1 || d.UploadThread > 32 {\n\t\td.UploadThread = 3\n\t}\n\n\tif d.RefreshToken != \"\" {\n\t\t// refresh token 直接主动刷新\n\t\td.AccessToken = \"\"\n\t\td.tm = &tokenManager{}\n\t} else {\n\t\t// 避免个人 token 刷新产生的多个登录，被动刷新\n\t\t// 默认过期时间90天，jwt exp 不可靠\n\t\td.tm = &tokenManager{\n\t\t\t// accessToken: d.AccessToken,\n\t\t\texpiredAt: time.Now().Add(90 * 24 * time.Hour),\n\t\t}\n\t}\n\n\t_, err := d.getAccessToken(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"init get access token error: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) Drop(ctx context.Context) error {\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfileLastId := int64(0)\n\tparentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]File, 0)\n\n\tfor fileLastId != -1 {\n\t\tfiles, err := d.getFiles(parentFileId, 100, fileLastId)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 目前123panAPI请求，trashed失效，只能通过遍历过滤\n\t\tfor i := range files.Data.FileList {\n\t\t\tif files.Data.FileList[i].Trashed == 0 {\n\t\t\t\tres = append(res, files.Data.FileList[i])\n\t\t\t}\n\t\t}\n\t\tfileLastId = files.Data.LastFileId\n\t}\n\treturn utils.SliceConvert(res, func(src File) (model.Obj, error) {\n\t\treturn src, nil\n\t})\n}\n\nfunc (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tfileId, _ := strconv.ParseInt(file.GetID(), 10, 64)\n\n\tif d.DirectLink {\n\t\tres, err := d.getDirectLink(fileId)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif d.DirectLinkPrivateKey == \"\" {\n\t\t\tduration := 365 * 24 * time.Hour // 缓存1年\n\t\t\treturn &model.Link{\n\t\t\t\tURL:        res.Data.URL,\n\t\t\t\tExpiration: &duration,\n\t\t\t}, nil\n\t\t}\n\n\t\tuid, err := d.getUID(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tduration := time.Duration(d.DirectLinkValidDuration) * time.Minute\n\n\t\tnewURL, err := d.SignURL(res.Data.URL, d.DirectLinkPrivateKey,\n\t\t\tuid, duration)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &model.Link{\n\t\t\tURL:        newURL,\n\t\t\tExpiration: &duration,\n\t\t}, nil\n\t}\n\n\tres, err := d.getDownloadInfo(fileId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{URL: res.Data.DownloadUrl}, nil\n}\n\nfunc (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tparentFileId, _ := strconv.ParseInt(parentDir.GetID(), 10, 64)\n\n\treturn d.mkdir(parentFileId, dirName)\n}\n\nfunc (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\ttoParentFileID, _ := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\n\treturn d.move(srcObj.(File).FileId, toParentFileID)\n}\n\nfunc (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tfileId, _ := strconv.ParseInt(srcObj.GetID(), 10, 64)\n\n\treturn d.rename(fileId, newName)\n}\n\nfunc (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// 尝试使用上传+MD5秒传功能实现复制\n\t// 1. 创建文件\n\t// parentFileID 父目录id，上传到根目录时填写 0\n\tparentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parse parentFileID error: %v\", err)\n\t}\n\tetag := srcObj.(File).Etag\n\tcreateResp, err := d.create(parentFileId, srcObj.GetName(), etag, srcObj.GetSize(), 2, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 是否秒传\n\tif createResp.Data.Reuse {\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *Open123) Remove(ctx context.Context, obj model.Obj) error {\n\tfileId, _ := strconv.ParseInt(obj.GetID(), 10, 64)\n\n\treturn d.trash(fileId)\n}\n\nfunc (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// 1. 创建文件\n\t// parentFileID 父目录id，上传到根目录时填写 0\n\tparentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse parentFileID error: %v\", err)\n\t}\n\n\t// 尝试 SHA1 秒传\n\tsha1Hash := file.GetHash().GetHash(utils.SHA1)\n\tif len(sha1Hash) == utils.SHA1.Width {\n\t\tresp, err := d.sha1Reuse(parentFileId, file.GetName(), sha1Hash, file.GetSize(), 2)\n\t\tif err == nil && resp.Data.Reuse {\n\t\t\treturn File{\n\t\t\t\tFileName: file.GetName(),\n\t\t\t\tSize:     file.GetSize(),\n\t\t\t\tFileId:   resp.Data.FileID,\n\t\t\t\tType:     2,\n\t\t\t\tSHA1:     sha1Hash,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// etag 文件md5\n\tetag := file.GetHash().GetHash(utils.MD5)\n\tif len(etag) < utils.MD5.Width {\n\t\t_, etag, err = stream.CacheFullAndHash(file, &up, utils.MD5)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tcreateResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 是否秒传\n\tif createResp.Data.Reuse {\n\t\t// 秒传成功才会返回正确的 FileID，否则为 0\n\t\tif createResp.Data.FileID != 0 {\n\t\t\treturn File{\n\t\t\t\tFileName: file.GetName(),\n\t\t\t\tSize:     file.GetSize(),\n\t\t\t\tFileId:   createResp.Data.FileID,\n\t\t\t\tType:     2,\n\t\t\t\tEtag:     etag,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// 2. 上传分片\n\terr = d.Upload(ctx, file, createResp, up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 3. 上传完毕\n\tfor range 60 {\n\t\tuploadCompleteResp, err := d.complete(createResp.Data.PreuploadID)\n\t\t// 返回错误代码未知，如：20103，文档也没有具体说\n\t\tif err == nil && uploadCompleteResp.Data.Completed && uploadCompleteResp.Data.FileID != 0 {\n\t\t\tup(100)\n\t\t\treturn File{\n\t\t\t\tFileName: file.GetName(),\n\t\t\t\tSize:     file.GetSize(),\n\t\t\t\tFileId:   uploadCompleteResp.Data.FileID,\n\t\t\t\tType:     2,\n\t\t\t\tEtag:     etag,\n\t\t\t}, nil\n\t\t}\n\t\t// 若接口返回的completed为 false 时，则需间隔1秒继续轮询此接口，获取上传最终结果。\n\t\ttime.Sleep(time.Second)\n\t}\n\treturn nil, fmt.Errorf(\"upload complete timeout\")\n}\n\nfunc (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tuserInfo, err := d.getUserInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp,\n\t\t\tUsedSpace:  userInfo.Data.SpaceUsed,\n\t\t},\n\t}, nil\n}\n\nfunc (d *Open123) OfflineDownload(ctx context.Context, url string, dir model.Obj, callback string) (int, error) {\n\treturn d.createOfflineDownloadTask(ctx, url, dir.GetID(), callback)\n}\n\nfunc (d *Open123) OfflineDownloadProcess(ctx context.Context, taskID int) (float64, int, error) {\n\treturn d.queryOfflineDownloadStatus(ctx, taskID)\n}\n\nvar (\n\t_ driver.Driver    = (*Open123)(nil)\n\t_ driver.PutResult = (*Open123)(nil)\n)\n"
  },
  {
    "path": "drivers/123_open/meta.go",
    "content": "package _123_open\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t//  refresh_token方式的AccessToken  【对个人开发者暂未开放】\n\tRefreshToken string `json:\"RefreshToken\" required:\"false\"`\n\n\t//  通过 https://www.123pan.com/developer 申请\n\tClientID     string `json:\"ClientID\" required:\"false\"`\n\tClientSecret string `json:\"ClientSecret\" required:\"false\"`\n\n\t//  直接写入AccessToken, AccessToken有过期时间，不建议直接填写\n\tAccessToken string `json:\"AccessToken\" required:\"false\"`\n\n\t//  用户名+密码方式登录的AccessToken可以兼容\n\t//Username string `json:\"username\" required:\"false\"`\n\t//Password string `json:\"password\" required:\"false\"`\n\n\t//  上传线程数\n\tUploadThread int `json:\"UploadThread\" type:\"number\" default:\"3\" help:\"the threads of upload\"`\n\n\t//  使用直链\n\tDirectLink              bool   `json:\"DirectLink\" type:\"bool\" default:\"false\" required:\"false\" help:\"use direct link when download file\"`\n\tDirectLinkPrivateKey    string `json:\"DirectLinkPrivateKey\" required:\"false\" help:\"private key for direct link, if URL authentication is enabled\"`\n\tDirectLinkValidDuration int64  `json:\"DirectLinkValidDuration\" type:\"number\" default:\"30\" required:\"false\" help:\"minutes, if URL authentication is enabled\"`\n\n\tdriver.RootID\n}\n\nvar config = driver.Config{\n\tName:        \"123 Open\",\n\tDefaultRoot: \"0\",\n\tLocalSort:   true,\n\tPreferProxy: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Open123{}\n\t})\n}\n"
  },
  {
    "path": "drivers/123_open/token.go",
    "content": "package _123_open\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\nvar (\n\tAccessToken  = \"https://open-api.123pan.com/api/v1/access_token\"\n\tRefreshToken = \"https://open-api.123pan.com/api/v1/oauth2/access_token\"\n)\n\ntype tokenManager struct {\n\t// accessToken  string\n\texpiredAt    time.Time\n\tmu           sync.Mutex\n\tblockRefresh bool\n}\n\nfunc (d *Open123) getAccessToken(forceRefresh bool) (string, error) {\n\ttm := d.tm\n\ttm.mu.Lock()\n\tdefer tm.mu.Unlock()\n\tif tm.blockRefresh {\n\t\treturn \"\", errors.New(\"Authentication expired\")\n\t}\n\tif !forceRefresh && d.AccessToken != \"\" && time.Now().Before(tm.expiredAt.Add(-5*time.Minute)) {\n\t\treturn d.AccessToken, nil\n\t}\n\tif err := d.flushAccessToken(); err != nil {\n\t\t// token expired and failed to refresh, block further refresh attempts\n\t\ttm.blockRefresh = true\n\t\treturn \"\", err\n\t}\n\treturn d.AccessToken, nil\n}\n\nfunc (d *Open123) flushAccessToken() error {\n\t// directly send request to avoid deadlock\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"authorization\": \"Bearer \" + d.AccessToken,\n\t\t\"platform\":      \"open_platform\",\n\t\t\"Content-Type\":  \"application/json\",\n\t})\n\n\tif d.ClientID != \"\" {\n\t\tif d.RefreshToken != \"\" {\n\t\t\tvar resp RefreshTokenResp\n\t\t\treq.SetQueryParam(\"client_id\", d.ClientID)\n\t\t\tif d.ClientSecret != \"\" {\n\t\t\t\treq.SetQueryParam(\"client_secret\", d.ClientSecret)\n\t\t\t}\n\t\t\treq.SetQueryParam(\"grant_type\", \"refresh_token\")\n\t\t\treq.SetQueryParam(\"refresh_token\", d.RefreshToken)\n\t\t\treq.SetResult(&resp)\n\t\t\tres, err := req.Execute(http.MethodPost, RefreshToken)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbody := res.Body()\n\t\t\tvar baseResp BaseResp\n\t\t\tif err = json.Unmarshal(body, &baseResp); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif baseResp.Code != 0 {\n\t\t\t\treturn fmt.Errorf(\"get access token failed: %s\", baseResp.Message)\n\t\t\t}\n\n\t\t\td.AccessToken = resp.AccessToken\n\t\t\t// add token expire time\n\t\t\td.tm.expiredAt = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)\n\t\t\td.RefreshToken = resp.RefreshToken\n\t\t\top.MustSaveDriverStorage(d)\n\t\t\td.tm.blockRefresh = false\n\t\t\treturn nil\n\t\t} else if d.ClientSecret != \"\" {\n\t\t\tvar resp AccessTokenResp\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"clientID\":     d.ClientID,\n\t\t\t\t\"clientSecret\": d.ClientSecret,\n\t\t\t})\n\t\t\treq.SetResult(&resp)\n\t\t\tres, err := req.Execute(http.MethodPost, AccessToken)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbody := res.Body()\n\t\t\tvar baseResp BaseResp\n\t\t\tif err = json.Unmarshal(body, &baseResp); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif baseResp.Code != 0 {\n\t\t\t\treturn fmt.Errorf(\"get access token failed: %s\", baseResp.Message)\n\t\t\t}\n\t\t\td.AccessToken = resp.Data.AccessToken\n\t\t\t// parse token expire time\n\t\t\td.tm.expiredAt, err = time.Parse(time.RFC3339, resp.Data.ExpiredAt)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"parse expire time failed: %w\", err)\n\t\t\t}\n\t\t\top.MustSaveDriverStorage(d)\n\t\t\td.tm.blockRefresh = false\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errors.New(\"no valid authentication method available\")\n}\n"
  },
  {
    "path": "drivers/123_open/types.go",
    "content": "package _123_open\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ApiInfo struct {\n\turl   string\n\tqps   int\n\ttoken chan struct{}\n}\n\nfunc (a *ApiInfo) Require() {\n\tif a.qps > 0 {\n\t\ta.token <- struct{}{}\n\t}\n}\n\nfunc (a *ApiInfo) Release() {\n\tif a.qps > 0 {\n\t\ttime.AfterFunc(time.Second, func() {\n\t\t\t<-a.token\n\t\t})\n\t}\n}\n\nfunc (a *ApiInfo) SetQPS(qps int) {\n\ta.qps = qps\n\ta.token = make(chan struct{}, qps)\n}\n\nfunc (a *ApiInfo) NowLen() int {\n\treturn len(a.token)\n}\n\nfunc InitApiInfo(url string, qps int) *ApiInfo {\n\treturn &ApiInfo{\n\t\turl:   url,\n\t\tqps:   qps,\n\t\ttoken: make(chan struct{}, qps),\n\t}\n}\n\ntype File struct {\n\tFileName     string `json:\"filename\"`\n\tSize         int64  `json:\"size\"`\n\tCreateAt     string `json:\"createAt\"`\n\tUpdateAt     string `json:\"updateAt\"`\n\tFileId       int64  `json:\"fileId\"`\n\tType         int    `json:\"type\"`\n\tEtag         string `json:\"etag\"`\n\tS3KeyFlag    string `json:\"s3KeyFlag\"`\n\tParentFileId int    `json:\"parentFileId\"`\n\tCategory     int    `json:\"category\"`\n\tStatus       int    `json:\"status\"`\n\tTrashed      int    `json:\"trashed\"`\n\tSHA1         string\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\tif len(f.SHA1) == utils.SHA1.Width && len(f.Etag) != utils.MD5.Width {\n\t\treturn utils.NewHashInfo(utils.SHA1, f.SHA1)\n\t}\n\treturn utils.NewHashInfo(utils.MD5, f.Etag)\n}\n\nfunc (f File) GetPath() string {\n\treturn \"\"\n}\n\nfunc (f File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) CreateTime() time.Time {\n\t// 返回的时间没有时区信息，默认 UTC+8\n\tloc := time.FixedZone(\"UTC+8\", 8*60*60)\n\tparsedTime, err := time.ParseInLocation(\"2006-01-02 15:04:05\", f.CreateAt, loc)\n\tif err != nil {\n\t\treturn time.Now()\n\t}\n\treturn parsedTime\n}\n\nfunc (f File) ModTime() time.Time {\n\t// 返回的时间没有时区信息，默认 UTC+8\n\tloc := time.FixedZone(\"UTC+8\", 8*60*60)\n\tparsedTime, err := time.ParseInLocation(\"2006-01-02 15:04:05\", f.UpdateAt, loc)\n\tif err != nil {\n\t\treturn time.Now()\n\t}\n\treturn parsedTime\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == 1\n}\n\nfunc (f File) GetID() string {\n\treturn strconv.FormatInt(f.FileId, 10)\n}\n\nvar _ model.Obj = (*File)(nil)\n\ntype BaseResp struct {\n\tCode     int    `json:\"code\"`\n\tMessage  string `json:\"message\"`\n\tXTraceID string `json:\"x-traceID\"`\n}\n\ntype AccessTokenResp struct {\n\tBaseResp\n\tData struct {\n\t\tAccessToken string `json:\"accessToken\"`\n\t\tExpiredAt   string `json:\"expiredAt\"`\n\t} `json:\"data\"`\n}\n\ntype RefreshTokenResp struct {\n\tAccessToken  string `json:\"access_token\"`\n\tExpiresIn    int    `json:\"expires_in\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tScope        string `json:\"scope\"`\n\tTokenType    string `json:\"token_type\"`\n}\n\ntype UserInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tUID uint64 `json:\"uid\"`\n\t\t// Username       string `json:\"username\"`\n\t\t// DisplayName    string `json:\"displayName\"`\n\t\t// HeadImage      string `json:\"headImage\"`\n\t\t// Passport       string `json:\"passport\"`\n\t\t// Mail           string `json:\"mail\"`\n\t\tSpaceUsed      int64 `json:\"spaceUsed\"`\n\t\tSpacePermanent int64 `json:\"spacePermanent\"`\n\t\tSpaceTemp      int64 `json:\"spaceTemp\"`\n\t\t// SpaceTempExpr  int64  `json:\"spaceTempExpr\"`\n\t\t// Vip            bool   `json:\"vip\"`\n\t\t// DirectTraffic  int64  `json:\"directTraffic\"`\n\t\t// IsHideUID      bool   `json:\"isHideUID\"`\n\t} `json:\"data\"`\n}\n\ntype FileListResp struct {\n\tBaseResp\n\tData struct {\n\t\tLastFileId int64  `json:\"lastFileId\"`\n\t\tFileList   []File `json:\"fileList\"`\n\t} `json:\"data\"`\n}\n\ntype DownloadInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tDownloadUrl string `json:\"downloadUrl\"`\n\t} `json:\"data\"`\n}\n\ntype DirectLinkResp struct {\n\tBaseResp\n\tData struct {\n\t\tURL string `json:\"url\"`\n\t} `json:\"data\"`\n}\n\n// 创建文件V2返回\ntype UploadCreateResp struct {\n\tBaseResp\n\tData struct {\n\t\tFileID      int64    `json:\"fileID\"`\n\t\tPreuploadID string   `json:\"preuploadID\"`\n\t\tReuse       bool     `json:\"reuse\"`\n\t\tSliceSize   int64    `json:\"sliceSize\"`\n\t\tServers     []string `json:\"servers\"`\n\t} `json:\"data\"`\n}\n\n// 上传完毕V2返回\ntype UploadCompleteResp struct {\n\tBaseResp\n\tData struct {\n\t\tCompleted bool  `json:\"completed\"`\n\t\tFileID    int64 `json:\"fileID\"`\n\t} `json:\"data\"`\n}\n\ntype SHA1ReuseResp struct {\n\tBaseResp\n\tData struct {\n\t\tFileID int64 `json:\"fileID\"`\n\t\tReuse  bool  `json:\"reuse\"`\n\t} `json:\"data\"`\n}\n\ntype OfflineDownloadResp struct {\n\tBaseResp\n\tData struct {\n\t\tTaskID int `json:\"taskID\"`\n\t} `json:\"data\"`\n}\n\ntype OfflineDownloadProcessResp struct {\n\tBaseResp\n\tData struct {\n\t\tProcess float64 `json:\"process\"`\n\t\tStatus  int     `json:\"status\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/123_open/upload.go",
    "content": "package _123_open\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\n// 创建文件 V2\nfunc (d *Open123) create(parentFileID int64, filename string, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {\n\tvar resp UploadCreateResp\n\t_, err := d.Request(UploadCreate, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"parentFileId\": parentFileID,\n\t\t\t\"filename\":     filename,\n\t\t\t\"etag\":         strings.ToLower(etag),\n\t\t\t\"size\":         size,\n\t\t\t\"duplicate\":    duplicate,\n\t\t\t\"containDir\":   containDir,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 上传分片 V2\nfunc (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createResp *UploadCreateResp, up driver.UpdateProgress) error {\n\tuploadDomain := createResp.Data.Servers[0]\n\tsize := file.GetSize()\n\tchunkSize := createResp.Data.SliceSize\n\n\tss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadNums := (size + chunkSize - 1) / chunkSize\n\tthread := min(int(uploadNums), d.UploadThread)\n\tthreadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\n\tfor partIndex := range uploadNums {\n\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\tbreak\n\t\t}\n\t\tpartIndex := partIndex\n\t\tpartNumber := partIndex + 1 // 分片号从1开始\n\t\toffset := partIndex * chunkSize\n\t\tsize := min(chunkSize, size-offset)\n\t\tvar reader io.ReadSeeker\n\t\tvar rateLimitedRd io.Reader\n\t\tsliceMD5 := \"\"\n\t\t// 表单\n\t\tb := bytes.NewBuffer(make([]byte, 0, 2048))\n\t\tthreadG.GoWithLifecycle(errgroup.Lifecycle{\n\t\t\tBefore: func(ctx context.Context) (err error) {\n\t\t\t\treader, err = ss.GetSectionReader(offset, size)\n\t\t\t\treturn\n\t\t\t},\n\t\t\tDo: func(ctx context.Context) (err error) {\n\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\tif sliceMD5 == \"\" {\n\t\t\t\t\t// 把耗时的计算放在这里，避免阻塞其他协程\n\t\t\t\t\tsliceMD5, err = utils.HashReader(utils.MD5, reader)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\t}\n\n\t\t\t\tb.Reset()\n\t\t\t\tw := multipart.NewWriter(b)\n\t\t\t\t// 添加表单字段\n\t\t\t\terr = w.WriteField(\"preuploadID\", createResp.Data.PreuploadID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = w.WriteField(\"sliceNo\", strconv.FormatInt(partNumber, 10))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = w.WriteField(\"sliceMD5\", sliceMD5)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// 写入文件内容\n\t\t\t\t_, err = w.CreateFormFile(\"slice\", fmt.Sprintf(\"%s.part%d\", file.GetName(), partNumber))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\theadSize := b.Len()\n\t\t\t\terr = w.Close()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\thead := bytes.NewReader(b.Bytes()[:headSize])\n\t\t\t\ttail := bytes.NewReader(b.Bytes()[headSize:])\n\t\t\t\trateLimitedRd = driver.NewLimitedUploadStream(ctx, io.MultiReader(head, reader, tail))\n\t\t\t\ttoken, err := d.getAccessToken(false)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// 创建请求并设置header\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+\"/upload/v2/file/slice\", rateLimitedRd)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// 设置请求头\n\t\t\t\treq.Header.Add(\"Authorization\", \"Bearer \"+token)\n\t\t\t\treq.Header.Add(\"Content-Type\", w.FormDataContentType())\n\t\t\t\treq.Header.Add(\"Platform\", \"open_platform\")\n\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\tif res.StatusCode != 200 {\n\t\t\t\t\treturn fmt.Errorf(\"slice %d upload failed, status code: %d\", partNumber, res.StatusCode)\n\t\t\t\t}\n\t\t\t\tb.Reset()\n\t\t\t\t_, err = b.ReadFrom(res.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tvar resp BaseResp\n\t\t\t\terr = json.Unmarshal(b.Bytes(), &resp)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif resp.Code != 0 {\n\t\t\t\t\treturn fmt.Errorf(\"slice %d upload failed: %s\", partNumber, resp.Message)\n\t\t\t\t}\n\n\t\t\t\tprogress := 100 * float64(threadG.Success()+1) / float64(uploadNums+1)\n\t\t\t\tup(progress)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tAfter: func(err error) {\n\t\t\t\tss.FreeSectionReader(reader)\n\t\t\t},\n\t\t})\n\t}\n\n\tif err := threadG.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// 上传完毕\nfunc (d *Open123) complete(preuploadID string) (*UploadCompleteResp, error) {\n\tvar resp UploadCompleteResp\n\t_, err := d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"preuploadID\": preuploadID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// SHA1 秒传\nfunc (d *Open123) sha1Reuse(parentFileID int64, filename string, sha1Hash string, size int64, duplicate int) (*SHA1ReuseResp, error) {\n\tvar resp SHA1ReuseResp\n\t_, err := d.Request(UploadSHA1Reuse, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"parentFileID\": parentFileID,\n\t\t\t\"filename\":     filename,\n\t\t\t\"sha1\":         strings.ToLower(sha1Hash),\n\t\t\t\"size\":         size,\n\t\t\t\"duplicate\":    duplicate,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/123_open/util.go",
    "content": "package _123_open\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展\n\tApi = \"https://open-api.123pan.com\"\n\n\tUserInfo        = InitApiInfo(Api+\"/api/v1/user/info\", 1)\n\tFileList        = InitApiInfo(Api+\"/api/v2/file/list\", 3)\n\tDownloadInfo    = InitApiInfo(Api+\"/api/v1/file/download_info\", 5)\n\tDirectLink      = InitApiInfo(Api+\"/api/v1/direct-link/url\", 5)\n\tMkdir           = InitApiInfo(Api+\"/upload/v1/file/mkdir\", 2)\n\tMove            = InitApiInfo(Api+\"/api/v1/file/move\", 1)\n\tRename          = InitApiInfo(Api+\"/api/v1/file/name\", 1)\n\tTrash           = InitApiInfo(Api+\"/api/v1/file/trash\", 2)\n\tUploadCreate    = InitApiInfo(Api+\"/upload/v2/file/create\", 2)\n\tUploadComplete  = InitApiInfo(Api+\"/upload/v2/file/upload_complete\", 0)\n\tUploadSHA1Reuse = InitApiInfo(Api+\"/upload/v2/file/sha1_reuse\", 2)\n\n\tOfflineDownload        = InitApiInfo(Api+\"/api/v1/offline/download\", 1)\n\tOfflineDownloadProcess = InitApiInfo(Api+\"/api/v1/offline/download/process\", 5)\n)\n\nfunc (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tfor {\n\t\ttoken, err := d.getAccessToken(false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treq := base.RestyClient.R()\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"authorization\": \"Bearer \" + token,\n\t\t\t\"platform\":      \"open_platform\",\n\t\t\t\"Content-Type\":  \"application/json\",\n\t\t})\n\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t\tif resp != nil {\n\t\t\treq.SetResult(resp)\n\t\t}\n\n\t\tlog.Debugf(\"API: %s, QPS: %d, NowLen: %d\", apiInfo.url, apiInfo.qps, apiInfo.NowLen())\n\n\t\tapiInfo.Require()\n\t\tdefer apiInfo.Release()\n\t\tres, err := req.Execute(method, apiInfo.url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbody := res.Body()\n\n\t\t// 解析为通用响应\n\t\tvar baseResp BaseResp\n\t\tif err = json.Unmarshal(body, &baseResp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif baseResp.Code == 0 {\n\t\t\treturn body, nil\n\t\t} else if baseResp.Code == 401 {\n\t\t\t// 强制刷新Token, 有小概率会 race condition 导致多次刷新Token，但不影响正确运行\n\t\t\tif _, err := d.getAccessToken(true); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else if baseResp.Code == 429 {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tlog.Warningf(\"API: %s, QPS: %d, 请求太频繁，对应API提示过多请减小QPS\", apiInfo.url, apiInfo.qps)\n\t\t} else {\n\t\t\treturn nil, errors.New(baseResp.Message)\n\t\t}\n\t}\n}\n\nfunc (d *Open123) SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {\n\t// 生成Unix时间戳\n\tts := time.Now().Add(validDuration).Unix()\n\n\t// 生成随机数（建议使用UUID，不能包含中划线（-））\n\trand := strings.ReplaceAll(uuid.New().String(), \"-\", \"\")\n\n\t// 解析URL\n\tobjURL, err := url.Parse(originURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// 待签名字符串，格式：path-timestamp-rand-uid-privateKey\n\tunsignedStr := fmt.Sprintf(\"%s-%d-%s-%d-%s\", objURL.Path, ts, rand, uid, privateKey)\n\tmd5Hash := md5.Sum([]byte(unsignedStr))\n\t// 生成鉴权参数，格式：timestamp-rand-uid-md5hash\n\tauthKey := fmt.Sprintf(\"%d-%s-%d-%x\", ts, rand, uid, md5Hash)\n\n\t// 添加鉴权参数到URL查询参数\n\tv := objURL.Query()\n\tv.Add(\"auth_key\", authKey)\n\tobjURL.RawQuery = v.Encode()\n\n\treturn objURL.String(), nil\n}\n\nfunc (d *Open123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {\n\tvar resp UserInfoResp\n\n\tif _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Open123) getUID(ctx context.Context) (uint64, error) {\n\tif d.UID != 0 {\n\t\treturn d.UID, nil\n\t}\n\tresp, err := d.getUserInfo(ctx)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\td.UID = resp.Data.UID\n\treturn resp.Data.UID, nil\n}\n\nfunc (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {\n\tvar resp FileListResp\n\n\t_, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(\n\t\t\tmap[string]string{\n\t\t\t\t\"parentFileId\": strconv.FormatInt(parentFileId, 10),\n\t\t\t\t\"limit\":        strconv.Itoa(limit),\n\t\t\t\t\"lastFileId\":   strconv.FormatInt(lastFileId, 10),\n\t\t\t\t\"trashed\":      \"false\",\n\t\t\t\t\"searchMode\":   \"\",\n\t\t\t\t\"searchData\":   \"\",\n\t\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {\n\tvar resp DownloadInfoResp\n\n\t_, err := d.Request(DownloadInfo, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"fileId\": strconv.FormatInt(fileId, 10),\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Open123) getDirectLink(fileId int64) (*DirectLinkResp, error) {\n\tvar resp DirectLinkResp\n\n\t_, err := d.Request(DirectLink, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"fileID\": strconv.FormatInt(fileId, 10),\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Open123) mkdir(parentID int64, name string) error {\n\t_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"parentID\": strconv.FormatInt(parentID, 10),\n\t\t\t\"name\":     name,\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) move(fileID, toParentFileID int64) error {\n\t_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileIDs\":        []int64{fileID},\n\t\t\t\"toParentFileID\": toParentFileID,\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) rename(fileId int64, fileName string) error {\n\t_, err := d.Request(Rename, http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileId\":   fileId,\n\t\t\t\"fileName\": fileName,\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) trash(fileId int64) error {\n\t_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileIDs\": []int64{fileId},\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) createOfflineDownloadTask(ctx context.Context, url string, dirID, callback string) (taskID int, err error) {\n\tbody := base.Json{\n\t\t\"url\":   url,\n\t\t\"dirID\": dirID,\n\t}\n\tif len(callback) > 0 {\n\t\tbody[\"callBackUrl\"] = callback\n\t}\n\tvar resp OfflineDownloadResp\n\t_, err = d.Request(OfflineDownload, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, &resp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn resp.Data.TaskID, nil\n}\n\nfunc (d *Open123) queryOfflineDownloadStatus(ctx context.Context, taskID int) (process float64, status int, err error) {\n\tvar resp OfflineDownloadProcessResp\n\t_, err = d.Request(OfflineDownloadProcess, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"taskID\": strconv.Itoa(taskID),\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn .0, 0, err\n\t}\n\treturn resp.Data.Process, resp.Data.Status, nil\n}\n"
  },
  {
    "path": "drivers/123_share/driver.go",
    "content": "package _123Share\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\n\t_123 \"github.com/OpenListTeam/OpenList/v4/drivers/123\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Pan123Share struct {\n\tmodel.Storage\n\tAddition\n\tapiRateLimit sync.Map\n\tref          *_123.Pan123\n}\n\nfunc (d *Pan123Share) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Pan123Share) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Pan123Share) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Pan123Share) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*_123.Pan123)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"ref: storage is not 123Pan\")\n}\n\nfunc (d *Pan123Share) Drop(ctx context.Context) error {\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\t// TODO return the files list, required\n\tfiles, err := d.getFiles(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn src, nil\n\t})\n}\n\nfunc (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\t// TODO return link of file, required\n\tif f, ok := file.(File); ok {\n\t\tdata := base.Json{\n\t\t\t\"shareKey\":  d.ShareKey,\n\t\t\t\"SharePwd\":  d.SharePwd,\n\t\t\t\"etag\":      f.Etag,\n\t\t\t\"fileId\":    f.FileId,\n\t\t\t\"s3keyFlag\": f.S3KeyFlag,\n\t\t\t\"size\":      f.Size,\n\t\t}\n\t\tresp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(data)\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdownloadUrl := utils.Json.Get(resp, \"data\", \"DownloadURL\").ToString()\n\t\tou, err := url.Parse(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tu_ := ou.String()\n\t\tnu := ou.Query().Get(\"params\")\n\t\tif nu != \"\" {\n\t\t\tdu, _ := base64.StdEncoding.DecodeString(nu)\n\t\t\tu, err := url.Parse(string(du))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tu_ = u.String()\n\t\t}\n\n\t\tlog.Debug(\"download url: \", u_)\n\t\tres, err := base.NoRedirectClient.R().SetHeader(\"Referer\", \"https://www.123pan.com/\").Get(u_)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debug(res.String())\n\t\tlink := model.Link{\n\t\t\tURL: u_,\n\t\t}\n\t\tlog.Debugln(\"res code: \", res.StatusCode())\n\t\tif res.StatusCode() == 302 {\n\t\t\tlink.URL = res.Header().Get(\"location\")\n\t\t} else if res.StatusCode() < 300 {\n\t\t\tlink.URL = utils.Json.Get(res.Body(), \"data\", \"redirect_url\").ToString()\n\t\t}\n\t\tlink.Header = http.Header{\n\t\t\t\"Referer\": []string{fmt.Sprintf(\"%s://%s/\", ou.Scheme, ou.Host)},\n\t\t}\n\t\treturn &link, nil\n\t}\n\treturn nil, fmt.Errorf(\"can't convert obj\")\n}\n\nfunc (d *Pan123Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t// TODO create folder, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO move obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t// TODO rename obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO copy obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123Share) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\t// TODO upload file, optional\n\treturn errs.NotSupport\n}\n\n//func (d *Pan123Share) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nfunc (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {\n\tvalue, _ := d.apiRateLimit.LoadOrStore(api,\n\t\trate.NewLimiter(rate.Every(700*time.Millisecond), 1))\n\tlimiter := value.(*rate.Limiter)\n\n\treturn limiter.Wait(ctx)\n}\n\nvar _ driver.Driver = (*Pan123Share)(nil)\n"
  },
  {
    "path": "drivers/123_share/meta.go",
    "content": "package _123Share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tShareKey string `json:\"sharekey\" required:\"true\"`\n\tSharePwd string `json:\"sharepassword\"`\n\tdriver.RootID\n\t//OrderBy        string `json:\"order_by\" type:\"select\" options:\"file_name,size,update_at\" default:\"file_name\"`\n\t//OrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tAccessToken string `json:\"accesstoken\" type:\"text\"`\n}\n\nvar config = driver.Config{\n\tName:        \"123PanShare\",\n\tLocalSort:   true,\n\tNoUpload:    true,\n\tDefaultRoot: \"0\",\n\tPreferProxy: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Pan123Share{}\n\t})\n}\n"
  },
  {
    "path": "drivers/123_share/types.go",
    "content": "package _123Share\n\nimport (\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype File struct {\n\tFileName    string    `json:\"FileName\"`\n\tSize        int64     `json:\"Size\"`\n\tUpdateAt    time.Time `json:\"UpdateAt\"`\n\tFileId      int64     `json:\"FileId\"`\n\tType        int       `json:\"Type\"`\n\tEtag        string    `json:\"Etag\"`\n\tS3KeyFlag   string    `json:\"S3KeyFlag\"`\n\tDownloadUrl string    `json:\"DownloadUrl\"`\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.MD5, f.Etag)\n}\n\nfunc (f File) GetPath() string {\n\treturn \"\"\n}\n\nfunc (f File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) ModTime() time.Time {\n\treturn f.UpdateAt\n}\nfunc (f File) CreateTime() time.Time {\n\treturn f.UpdateAt\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == 1\n}\n\nfunc (f File) GetID() string {\n\treturn strconv.FormatInt(f.FileId, 10)\n}\n\nfunc (f File) Thumb() string {\n\tif f.DownloadUrl == \"\" {\n\t\treturn \"\"\n\t}\n\tdu, err := url.Parse(f.DownloadUrl)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdu.Path = strings.TrimSuffix(du.Path, \"_24_24\") + \"_70_70\"\n\tquery := du.Query()\n\tquery.Set(\"w\", \"70\")\n\tquery.Set(\"h\", \"70\")\n\tif !query.Has(\"type\") {\n\t\tquery.Set(\"type\", strings.TrimPrefix(path.Base(f.FileName), \".\"))\n\t}\n\tif !query.Has(\"trade_key\") {\n\t\tquery.Set(\"trade_key\", \"123pan-thumbnail\")\n\t}\n\tdu.RawQuery = query.Encode()\n\treturn du.String()\n}\n\nvar _ model.Obj = (*File)(nil)\nvar _ model.Thumb = (*File)(nil)\n\n//func (f File) Thumb() string {\n//\n//}\n//var _ model.Thumb = (*File)(nil)\n\ntype Files struct {\n\t//BaseResp\n\tData struct {\n\t\tInfoList []File `json:\"InfoList\"`\n\t\tNext     string `json:\"Next\"`\n\t} `json:\"data\"`\n}\n\n//type DownResp struct {\n//\t//BaseResp\n//\tData struct {\n//\t\tDownloadUrl string `json:\"DownloadUrl\"`\n//\t} `json:\"data\"`\n//}\n"
  },
  {
    "path": "drivers/123_share/util.go",
    "content": "package _123Share\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nconst (\n\tApi          = \"https://www.123pan.com/api\"\n\tAApi         = \"https://www.123pan.com/a/api\"\n\tBApi         = \"https://www.123pan.com/b/api\"\n\tMainApi      = BApi\n\tFileList     = MainApi + \"/share/get\"\n\tDownloadInfo = MainApi + \"/share/download/info\"\n\t//AuthKeySalt      = \"8-8D$sL8gPjom7bk#cY\"\n)\n\nfunc signPath(path string, os string, version string) (k string, v string) {\n\ttable := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}\n\trandom := fmt.Sprintf(\"%.f\", math.Round(1e7*rand.Float64()))\n\tnow := time.Now().In(time.FixedZone(\"CST\", 8*3600))\n\ttimestamp := fmt.Sprint(now.Unix())\n\tnowStr := []byte(now.Format(\"200601021504\"))\n\tfor i := 0; i < len(nowStr); i++ {\n\t\tnowStr[i] = table[nowStr[i]-48]\n\t}\n\ttimeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))\n\tdata := strings.Join([]string{timestamp, random, path, os, version, timeSign}, \"|\")\n\tdataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))\n\treturn timeSign, strings.Join([]string{timestamp, random, dataSign}, \"-\")\n}\n\nfunc GetApi(rawUrl string) string {\n\tu, _ := url.Parse(rawUrl)\n\tquery := u.Query()\n\tquery.Add(signPath(u.Path, \"web\", \"3\"))\n\tu.RawQuery = query.Encode()\n\treturn u.String()\n}\n\nfunc (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tif d.ref != nil {\n\t\treturn d.ref.Request(url, method, callback, resp)\n\t}\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"origin\":        \"https://www.123pan.com\",\n\t\t\"referer\":       \"https://www.123pan.com/\",\n\t\t\"authorization\": \"Bearer \" + d.AccessToken,\n\t\t\"user-agent\":    \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client\",\n\t\t\"platform\":      \"web\",\n\t\t\"app-version\":   \"3\",\n\t\t//\"user-agent\":    base.UserAgent,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, GetApi(url))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbody := res.Body()\n\tcode := utils.Json.Get(body, \"code\").ToInt()\n\tif code != 0 {\n\t\treturn nil, errors.New(jsoniter.Get(body, \"message\").ToString())\n\t}\n\treturn body, nil\n}\n\nfunc (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {\n\tpage := 1\n\tres := make([]File, 0)\n\tfor {\n\t\tif err := d.APIRateLimit(ctx, FileList); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar resp Files\n\t\tquery := map[string]string{\n\t\t\t\"limit\":          \"100\",\n\t\t\t\"next\":           \"0\",\n\t\t\t\"orderBy\":        \"file_id\",\n\t\t\t\"orderDirection\": \"desc\",\n\t\t\t\"parentFileId\":   parentId,\n\t\t\t\"Page\":           strconv.Itoa(page),\n\t\t\t\"shareKey\":       d.ShareKey,\n\t\t\t\"SharePwd\":       d.SharePwd,\n\t\t}\n\t\t_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpage++\n\t\tres = append(res, resp.Data.InfoList...)\n\t\tif len(resp.Data.InfoList) == 0 || resp.Data.Next == \"-1\" {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn res, nil\n}\n\n// do others that not defined in Driver interface\n"
  },
  {
    "path": "drivers/139/driver.go",
    "content": "package _139\n\nimport (\n\t\"context\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Yun139 struct {\n\tmodel.Storage\n\tAddition\n\tcron              *cron.Cron\n\tAccount           string\n\tref               *Yun139\n\tPersonalCloudHost string\n\tRootPath          string\n}\n\nfunc (d *Yun139) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Yun139) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Yun139) Init(ctx context.Context) error {\n\tif d.ref == nil {\n\t\tif len(d.Authorization) == 0 {\n\t\t\tif d.Username != \"\" && d.Password != \"\" {\n\t\t\t\tlog.Infof(\"139yun: authorization is empty, trying to login with password.\")\n\t\t\t\tnewAuth, err := d.loginWithPassword()\n\t\t\t\tlog.Debugf(\"newAuth: Ok: %s\", newAuth)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"login with password failed: %w\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"authorization is empty and username/password is not provided\")\n\t\t\t}\n\t\t}\n\t\terr := d.refreshToken()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Query Route Policy\n\t\tvar resp QueryRoutePolicyResp\n\t\t_, err = d.requestRoute(base.Json{\n\t\t\t\"userInfo\": base.Json{\n\t\t\t\t\"userType\":    1,\n\t\t\t\t\"accountType\": 1,\n\t\t\t\t\"accountName\": d.Account,\n\t\t\t},\n\t\t\t\"modAddrType\": 1,\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, policyItem := range resp.Data.RoutePolicyList {\n\t\t\tif policyItem.ModName == \"personal\" {\n\t\t\t\td.PersonalCloudHost = policyItem.HttpsUrl\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(d.PersonalCloudHost) == 0 {\n\t\t\treturn fmt.Errorf(\"PersonalCloudHost is empty\")\n\t\t}\n\n\t\td.cron = cron.NewCron(time.Hour * 12)\n\t\td.cron.Do(func() {\n\t\t\terr := d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"%+v\", err)\n\t\t\t}\n\t\t})\n\t}\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tif len(d.Addition.RootFolderID) == 0 {\n\t\t\td.RootFolderID = \"/\"\n\t\t}\n\tcase MetaPersonal:\n\t\tif len(d.Addition.RootFolderID) == 0 {\n\t\t\td.RootFolderID = \"root\"\n\t\t}\n\tcase MetaGroup:\n\t\tif len(d.Addition.RootFolderID) == 0 {\n\t\t\td.RootFolderID = d.CloudID\n\t\t}\n\t\t_, err := d.groupGetFiles(d.RootFolderID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase MetaFamily:\n\t\tif len(d.Addition.RootFolderID) == 0 {\n\t\t\t// Attempt to obtain data.path as the root via a query and persist it.\n\t\t\tif root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != \"\" {\n\t\t\t\td.RootFolderID = root\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t}\n\t\t}\n\t\t_, err := d.familyGetFiles(d.RootFolderID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn nil\n}\n\nfunc (d *Yun139) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*Yun139)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *Yun139) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\treturn d.personalGetFiles(dir.GetID())\n\tcase MetaPersonal:\n\t\treturn d.getFiles(dir.GetID())\n\tcase MetaFamily:\n\t\treturn d.familyGetFiles(dir.GetID())\n\tcase MetaGroup:\n\t\treturn d.groupGetFiles(dir.GetID())\n\tdefault:\n\t\treturn nil, errs.NotImplement\n\t}\n}\n\nfunc (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar url string\n\tvar err error\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\turl, err = d.personalGetLink(file.GetID())\n\tcase MetaPersonal:\n\t\turl, err = d.getLink(file.GetID())\n\tcase MetaFamily:\n\t\turl, err = d.familyGetLink(file.GetID(), file.GetPath())\n\tcase MetaGroup:\n\t\turl, err = d.groupGetLink(file.GetID(), file.GetPath())\n\tdefault:\n\t\treturn nil, errs.NotImplement\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{URL: url}, nil\n}\n\nfunc (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tvar err error\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tdata := base.Json{\n\t\t\t\"parentFileId\":   parentDir.GetID(),\n\t\t\t\"name\":           dirName,\n\t\t\t\"description\":    \"\",\n\t\t\t\"type\":           \"folder\",\n\t\t\t\"fileRenameMode\": \"force_rename\",\n\t\t}\n\t\tpathname := \"/file/create\"\n\t\t_, err = d.personalPost(pathname, data, nil)\n\tcase MetaPersonal:\n\t\tdata := base.Json{\n\t\t\t\"createCatalogExtReq\": base.Json{\n\t\t\t\t\"parentCatalogID\": parentDir.GetID(),\n\t\t\t\t\"newCatalogName\":  dirName,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/personalCloud/catalog/v1.0/createCatalogExt\"\n\t\t_, err = d.post(pathname, data, nil)\n\tcase MetaFamily:\n\t\tdata := base.Json{\n\t\t\t\"cloudID\": d.CloudID,\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t\t\"docLibName\": dirName,\n\t\t\t\"path\":       path.Join(parentDir.GetPath(), parentDir.GetID()),\n\t\t}\n\t\tpathname := \"/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc\"\n\t\t_, err = d.post(pathname, data, nil)\n\tcase MetaGroup:\n\t\tdata := base.Json{\n\t\t\t\"catalogName\": dirName,\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t\t\"groupID\":      d.CloudID,\n\t\t\t\"parentFileId\": parentDir.GetID(),\n\t\t\t\"path\":         path.Join(parentDir.GetPath(), parentDir.GetID()),\n\t\t}\n\t\tpathname := \"/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog\"\n\t\t_, err = d.post(pathname, data, nil)\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\treturn err\n}\n\nfunc (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tdata := base.Json{\n\t\t\t\"fileIds\":        []string{srcObj.GetID()},\n\t\t\t\"toParentFileId\": dstDir.GetID(),\n\t\t}\n\t\tpathname := \"/file/batchMove\"\n\t\t_, err := d.personalPost(pathname, data, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn srcObj, nil\n\tcase MetaGroup:\n\t\tvar contentList []string\n\t\tvar catalogList []string\n\t\tif srcObj.IsDir() {\n\t\t\tcatalogList = append(catalogList, srcObj.GetID())\n\t\t} else {\n\t\t\tcontentList = append(contentList, srcObj.GetID())\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"taskType\":    3,\n\t\t\t\"srcType\":     2,\n\t\t\t\"srcGroupID\":  d.CloudID,\n\t\t\t\"destType\":    2,\n\t\t\t\"destGroupID\": d.CloudID,\n\t\t\t\"destPath\":    dstDir.GetPath(),\n\t\t\t\"contentList\": contentList,\n\t\t\t\"catalogList\": catalogList,\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/group-rebuild/task/v1.0/createBatchOprTask\"\n\t\t_, err := d.post(pathname, data, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn srcObj, nil\n\tcase MetaPersonal:\n\t\tvar contentInfoList []string\n\t\tvar catalogInfoList []string\n\t\tif srcObj.IsDir() {\n\t\t\tcatalogInfoList = append(catalogInfoList, srcObj.GetID())\n\t\t} else {\n\t\t\tcontentInfoList = append(contentInfoList, srcObj.GetID())\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"createBatchOprTaskReq\": base.Json{\n\t\t\t\t\"taskType\":   3,\n\t\t\t\t\"actionType\": \"304\",\n\t\t\t\t\"taskInfo\": base.Json{\n\t\t\t\t\t\"contentInfoList\": contentInfoList,\n\t\t\t\t\t\"catalogInfoList\": catalogInfoList,\n\t\t\t\t\t\"newCatalogID\":    dstDir.GetID(),\n\t\t\t\t},\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask\"\n\t\t_, err := d.post(pathname, data, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn srcObj, nil\n\tcase MetaFamily:\n\t\tpathname := \"/isbo/openApi/createBatchOprTask\"\n\t\tvar contentList []string\n\t\tvar catalogList []string\n\t\tif srcObj.IsDir() {\n\t\t\tcatalogList = append(catalogList, path.Join(srcObj.GetPath(), srcObj.GetID()))\n\t\t} else {\n\t\t\tcontentList = append(contentList, path.Join(srcObj.GetPath(), srcObj.GetID()))\n\t\t}\n\n\t\tbody := base.Json{\n\t\t\t\"catalogList\": catalogList,\n\t\t\t\"accountInfo\": base.Json{\n\t\t\t\t\"accountName\": d.getAccount(),\n\t\t\t\t\"accountType\": \"1\",\n\t\t\t},\n\t\t\t\"contentList\":   contentList,\n\t\t\t\"destCatalogID\": dstDir.GetID(),\n\t\t\t\"destGroupID\":   d.CloudID,\n\t\t\t\"destPath\":      path.Join(dstDir.GetPath(), dstDir.GetID()),\n\t\t\t\"destType\":      0,\n\t\t\t\"srcGroupID\":    d.CloudID,\n\t\t\t\"srcType\":       0,\n\t\t\t\"taskType\":      3,\n\t\t}\n\n\t\tvar resp CreateBatchOprTaskResp\n\t\t_, err := d.isboPost(pathname, body, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debugf(\"[139] Move MetaFamily CreateBatchOprTaskResp.Result.ResultCode: %s\", resp.Result.ResultCode)\n\t\tif resp.Result.ResultCode != \"0\" {\n\t\t\treturn nil, fmt.Errorf(\"failed to move in family cloud: %s\", resp.Result.ResultDesc)\n\t\t}\n\t\treturn srcObj, nil\n\tdefault:\n\t\treturn nil, errs.NotImplement\n\t}\n}\n\nfunc (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tvar err error\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tdata := base.Json{\n\t\t\t\"fileId\":      srcObj.GetID(),\n\t\t\t\"name\":        newName,\n\t\t\t\"description\": \"\",\n\t\t}\n\t\tpathname := \"/file/update\"\n\t\t_, err = d.personalPost(pathname, data, nil)\n\tcase MetaPersonal:\n\t\tvar data base.Json\n\t\tvar pathname string\n\t\tif srcObj.IsDir() {\n\t\t\tdata = base.Json{\n\t\t\t\t\"catalogID\":   srcObj.GetID(),\n\t\t\t\t\"catalogName\": newName,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpathname = \"/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo\"\n\t\t} else {\n\t\t\tdata = base.Json{\n\t\t\t\t\"contentID\":   srcObj.GetID(),\n\t\t\t\t\"contentName\": newName,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpathname = \"/orchestration/personalCloud/content/v1.0/updateContentInfo\"\n\t\t}\n\t\t_, err = d.post(pathname, data, nil)\n\tcase MetaGroup:\n\t\tvar data base.Json\n\t\tvar pathname string\n\t\tif srcObj.IsDir() {\n\t\t\tdata = base.Json{\n\t\t\t\t\"groupID\":           d.CloudID,\n\t\t\t\t\"modifyCatalogID\":   srcObj.GetID(),\n\t\t\t\t\"modifyCatalogName\": newName,\n\t\t\t\t\"path\":              srcObj.GetPath(),\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpathname = \"/orchestration/group-rebuild/catalog/v1.0/modifyGroupCatalog\"\n\t\t} else {\n\t\t\tdata = base.Json{\n\t\t\t\t\"groupID\":     d.CloudID,\n\t\t\t\t\"contentID\":   srcObj.GetID(),\n\t\t\t\t\"contentName\": newName,\n\t\t\t\t\"path\":        srcObj.GetPath(),\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpathname = \"/orchestration/group-rebuild/content/v1.0/modifyGroupContent\"\n\t\t}\n\t\t_, err = d.post(pathname, data, nil)\n\tcase MetaFamily:\n\t\tvar data base.Json\n\t\tvar pathname string\n\t\tif srcObj.IsDir() {\n\t\t\tpathname = \"/modifyCloudDocV2\"\n\t\t\tdata = base.Json{\n\t\t\t\t\"catalogType\": 3,\n\t\t\t\t\"cloudID\":     d.CloudID,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": \"1\",\n\t\t\t\t},\n\t\t\t\t\"docLibName\":   newName,\n\t\t\t\t\"docLibraryID\": srcObj.GetID(),\n\t\t\t\t\"path\":         path.Join(srcObj.GetPath(), srcObj.GetID()),\n\t\t\t}\n\t\t\tvar resp ModifyCloudDocV2Resp\n\t\t\t_, err = d.andAlbumRequest(pathname, data, &resp)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif resp.Result.ResultCode != \"0\" {\n\t\t\t\treturn fmt.Errorf(\"failed to rename family folder: %s\", resp.Result.ResultDesc)\n\t\t\t}\n\t\t\treturn nil\n\t\t} else {\n\t\t\tdata = base.Json{\n\t\t\t\t\"contentID\":   srcObj.GetID(),\n\t\t\t\t\"contentName\": newName,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t\t\"path\": srcObj.GetPath(),\n\t\t\t}\n\t\t\tpathname = \"/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyContentInfo\"\n\t\t}\n\t\t_, err = d.post(pathname, data, nil)\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\treturn err\n}\n\nfunc (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tvar err error\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tdata := base.Json{\n\t\t\t\"fileIds\":        []string{srcObj.GetID()},\n\t\t\t\"toParentFileId\": dstDir.GetID(),\n\t\t}\n\t\tpathname := \"/file/batchCopy\"\n\t\t_, err := d.personalPost(pathname, data, nil)\n\t\treturn err\n\tcase MetaPersonal:\n\t\tvar contentInfoList []string\n\t\tvar catalogInfoList []string\n\t\tif srcObj.IsDir() {\n\t\t\tcatalogInfoList = append(catalogInfoList, srcObj.GetID())\n\t\t} else {\n\t\t\tcontentInfoList = append(contentInfoList, srcObj.GetID())\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"createBatchOprTaskReq\": base.Json{\n\t\t\t\t\"taskType\":   3,\n\t\t\t\t\"actionType\": 309,\n\t\t\t\t\"taskInfo\": base.Json{\n\t\t\t\t\t\"contentInfoList\": contentInfoList,\n\t\t\t\t\t\"catalogInfoList\": catalogInfoList,\n\t\t\t\t\t\"newCatalogID\":    dstDir.GetID(),\n\t\t\t\t},\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask\"\n\t\t_, err = d.post(pathname, data, nil)\n\tcase MetaGroup:\n\t\terr = d.handleMetaGroupCopy(ctx, srcObj, dstDir)\n\tcase MetaFamily:\n\t\tpathname := \"/copyContentCatalog\"\n\t\tvar sourceContentIDs []string\n\t\tvar sourceCatalogIDs []string\n\t\tif srcObj.IsDir() {\n\t\t\tsourceCatalogIDs = append(sourceCatalogIDs, srcObj.GetID())\n\t\t} else {\n\t\t\tsourceContentIDs = append(sourceContentIDs, srcObj.GetID())\n\t\t}\n\n\t\tbody := base.Json{\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"accountType\":   \"1\",\n\t\t\t\t\"accountUserId\": d.ref.UserDomainID,\n\t\t\t},\n\t\t\t\"destCatalogID\":    dstDir.GetID(),\n\t\t\t\"destCloudID\":      d.CloudID,\n\t\t\t\"sourceCatalogIDs\": sourceCatalogIDs,\n\t\t\t\"sourceCloudID\":    d.CloudID,\n\t\t\t\"sourceContentIDs\": sourceContentIDs,\n\t\t}\n\n\t\tvar resp base.Json // Assuming a generic JSON response for success/failure\n\t\t_, err = d.andAlbumRequest(pathname, body, &resp)\n\t\t// For now, we assume no error means success.\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\treturn err\n}\n\nfunc (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tdata := base.Json{\n\t\t\t\"fileIds\": []string{obj.GetID()},\n\t\t}\n\t\tpathname := \"/recyclebin/batchTrash\"\n\t\t_, err := d.personalPost(pathname, data, nil)\n\t\treturn err\n\tcase MetaGroup:\n\t\tvar contentList []string\n\t\tvar catalogList []string\n\t\t// 必须使用完整路径删除\n\t\tif obj.IsDir() {\n\t\t\tcatalogList = append(catalogList, obj.GetPath())\n\t\t} else {\n\t\t\tcontentList = append(contentList, path.Join(obj.GetPath(), obj.GetID()))\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"taskType\":    2,\n\t\t\t\"srcGroupID\":  d.CloudID,\n\t\t\t\"contentList\": contentList,\n\t\t\t\"catalogList\": catalogList,\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/group-rebuild/task/v1.0/createBatchOprTask\"\n\t\t_, err := d.post(pathname, data, nil)\n\t\treturn err\n\tcase MetaPersonal:\n\t\tfallthrough\n\tcase MetaFamily:\n\t\tvar contentInfoList []string\n\t\tvar catalogInfoList []string\n\t\tif obj.IsDir() {\n\t\t\tcatalogInfoList = append(catalogInfoList, obj.GetID())\n\t\t} else {\n\t\t\tcontentInfoList = append(contentInfoList, obj.GetID())\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"createBatchOprTaskReq\": base.Json{\n\t\t\t\t\"taskType\":   2,\n\t\t\t\t\"actionType\": 201,\n\t\t\t\t\"taskInfo\": base.Json{\n\t\t\t\t\t\"newCatalogID\":    \"\",\n\t\t\t\t\t\"contentInfoList\": contentInfoList,\n\t\t\t\t\t\"catalogInfoList\": catalogInfoList,\n\t\t\t\t},\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask\"\n\t\tif d.isFamily() {\n\t\t\tdata = base.Json{\n\t\t\t\t\"catalogList\": catalogInfoList,\n\t\t\t\t\"contentList\": contentInfoList,\n\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t},\n\t\t\t\t\"sourceCloudID\":     d.CloudID,\n\t\t\t\t\"sourceCatalogType\": 1002,\n\t\t\t\t\"taskType\":          2,\n\t\t\t\t\"path\":              obj.GetPath(),\n\t\t\t}\n\t\t\tpathname = \"/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask\"\n\t\t}\n\t\t_, err := d.post(pathname, data, nil)\n\t\treturn err\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n}\n\nfunc (d *Yun139) getPartSize(size int64) int64 {\n\tif d.CustomUploadPartSize != 0 {\n\t\treturn d.CustomUploadPartSize\n\t}\n\t// 网盘对于分片数量存在上限\n\tif size/utils.GB > 30 {\n\t\treturn 512 * utils.MB\n\t}\n\treturn 100 * utils.MB\n}\n\nfunc (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tvar err error\n\t\tfullHash := stream.GetHash().GetHash(utils.SHA256)\n\t\tif len(fullHash) != utils.SHA256.Width {\n\t\t\t_, fullHash, err = streamPkg.CacheFullAndHash(stream, &up, utils.SHA256)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tsize := stream.GetSize()\n\t\tpartSize := d.getPartSize(size)\n\t\tpart := int64(1)\n\t\tif size > partSize {\n\t\t\tpart = (size + partSize - 1) / partSize\n\t\t}\n\n\t\t// 生成所有 partInfos\n\t\tpartInfos := make([]PartInfo, 0, part)\n\t\tfor i := int64(0); i < part; i++ {\n\t\t\tif utils.IsCanceled(ctx) {\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\t\t\tstart := i * partSize\n\t\t\tbyteSize := min(size-start, partSize)\n\t\t\tpartNumber := i + 1\n\t\t\tpartInfo := PartInfo{\n\t\t\t\tPartNumber: partNumber,\n\t\t\t\tPartSize:   byteSize,\n\t\t\t\tParallelHashCtx: ParallelHashCtx{\n\t\t\t\t\tPartOffset: start,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpartInfos = append(partInfos, partInfo)\n\t\t}\n\n\t\t// 筛选出前 100 个 partInfos\n\t\tfirstPartInfos := partInfos\n\t\tif len(firstPartInfos) > 100 {\n\t\t\tfirstPartInfos = firstPartInfos[:100]\n\t\t}\n\n\t\t// 创建任务，获取上传信息和前100个分片的上传地址\n\t\tdata := base.Json{\n\t\t\t\"contentHash\":          fullHash,\n\t\t\t\"contentHashAlgorithm\": \"SHA256\",\n\t\t\t\"contentType\":          \"application/octet-stream\",\n\t\t\t\"parallelUpload\":       false,\n\t\t\t\"partInfos\":            firstPartInfos,\n\t\t\t\"size\":                 size,\n\t\t\t\"parentFileId\":         dstDir.GetID(),\n\t\t\t\"name\":                 stream.GetName(),\n\t\t\t\"type\":                 \"file\",\n\t\t\t\"fileRenameMode\":       \"auto_rename\",\n\t\t}\n\t\tpathname := \"/file/create\"\n\t\tvar resp PersonalUploadResp\n\t\t_, err = d.personalPost(pathname, data, &resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// 判断文件是否已存在\n\t\t// resp.Data.Exist: true 已存在同名文件且校验相同，云端不会重复增加文件，无需手动处理冲突\n\t\tif resp.Data.Exist {\n\t\t\treturn nil\n\t\t}\n\n\t\t// 判断文件是否支持快传\n\t\t// resp.Data.RapidUpload: true 支持快传，但此处直接检测是否返回分片的上传地址\n\t\t// 快传的情况下同样需要手动处理冲突\n\t\tif resp.Data.PartInfos != nil {\n\t\t\t// Progress\n\t\t\tp := driver.NewProgress(size, up)\n\t\t\trateLimited := driver.NewLimitedUploadStream(ctx, stream)\n\n\t\t\t// 先上传前100个分片\n\t\t\terr = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, rateLimited, p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// 如果还有剩余分片，分批获取上传地址并上传\n\t\t\tfor i := 100; i < len(partInfos); i += 100 {\n\t\t\t\tend := min(i+100, len(partInfos))\n\t\t\t\tbatchPartInfos := partInfos[i:end]\n\t\t\t\tmoredata := base.Json{\n\t\t\t\t\t\"fileId\":    resp.Data.FileId,\n\t\t\t\t\t\"uploadId\":  resp.Data.UploadId,\n\t\t\t\t\t\"partInfos\": batchPartInfos,\n\t\t\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\t\t\"accountType\": 1,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tpathname := \"/file/getUploadUrl\"\n\t\t\t\tvar moreresp PersonalUploadUrlResp\n\t\t\t\t_, err = d.personalPost(pathname, moredata, &moreresp)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 全部分片上传完毕后，complete\n\t\t\tdata = base.Json{\n\t\t\t\t\"contentHash\":          fullHash,\n\t\t\t\t\"contentHashAlgorithm\": \"SHA256\",\n\t\t\t\t\"fileId\":               resp.Data.FileId,\n\t\t\t\t\"uploadId\":             resp.Data.UploadId,\n\t\t\t}\n\t\t\t_, err = d.personalPost(\"/file/complete\", data, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// 处理冲突\n\t\tif resp.Data.FileName != stream.GetName() {\n\t\t\tlog.Debugf(\"[139] conflict detected: %s != %s\", resp.Data.FileName, stream.GetName())\n\t\t\t// 给服务器一定时间处理数据，避免无法刷新文件列表\n\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\t// 刷新并获取文件列表\n\t\t\tfiles, err := d.List(ctx, dstDir, model.ListArgs{Refresh: true})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 删除旧文件\n\t\t\tfor _, file := range files {\n\t\t\t\tif file.GetName() == stream.GetName() {\n\t\t\t\t\tlog.Debugf(\"[139] conflict: removing old: %s\", file.GetName())\n\t\t\t\t\t// 删除前重命名旧文件，避免仍旧冲突\n\t\t\t\t\terr = d.Rename(ctx, file, stream.GetName()+random.String(4))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\terr = d.Remove(ctx, file)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 重命名新文件\n\t\t\tfor _, file := range files {\n\t\t\t\tif file.GetName() == resp.Data.FileName {\n\t\t\t\t\tlog.Debugf(\"[139] conflict: renaming new: %s => %s\", file.GetName(), stream.GetName())\n\t\t\t\t\terr = d.Rename(ctx, file, stream.GetName())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase MetaPersonal:\n\t\tfallthrough\n\tcase MetaGroup:\n\t\tfallthrough\n\tcase MetaFamily:\n\t\t// 处理冲突\n\t\t// 获取文件列表\n\t\tfiles, err := d.List(ctx, dstDir, model.ListArgs{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 删除旧文件\n\t\tfor _, file := range files {\n\t\t\tif file.GetName() == stream.GetName() {\n\t\t\t\tlog.Debugf(\"[139] conflict: removing old: %s\", file.GetName())\n\t\t\t\t// 删除前重命名旧文件，避免仍旧冲突\n\t\t\t\terr = d.Rename(ctx, file, stream.GetName()+random.String(4))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = d.Remove(ctx, file)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tvar reportSize int64\n\t\tif d.ReportRealSize {\n\t\t\treportSize = stream.GetSize()\n\t\t} else {\n\t\t\treportSize = 0\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"manualRename\": 2,\n\t\t\t\"operation\":    0,\n\t\t\t\"fileCount\":    1,\n\t\t\t\"totalSize\":    reportSize,\n\t\t\t\"uploadContentList\": []base.Json{{\n\t\t\t\t\"contentName\": stream.GetName(),\n\t\t\t\t\"contentSize\": reportSize,\n\t\t\t\t// \"digest\": \"5a3231986ce7a6b46e408612d385bafa\"\n\t\t\t}},\n\t\t\t\"parentCatalogID\": dstDir.GetID(),\n\t\t\t\"newCatalogName\":  \"\",\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t}\n\t\tpathname := \"/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest\"\n\t\tif d.isFamily() || d.Addition.Type == MetaGroup {\n\t\t\tuploadPath := path.Join(dstDir.GetPath(), dstDir.GetID())\n\t\t\t// if dstDir is root folder\n\t\t\tif dstDir.GetID() == d.RootFolderID {\n\t\t\t\tuploadPath = d.RootPath\n\t\t\t}\n\t\t\tdata = d.newJson(base.Json{\n\t\t\t\t\"fileCount\":    1,\n\t\t\t\t\"manualRename\": 2,\n\t\t\t\t\"operation\":    0,\n\t\t\t\t\"path\":         uploadPath,\n\t\t\t\t\"seqNo\":        random.String(32), // 序列号不能为空\n\t\t\t\t\"totalSize\":    reportSize,\n\t\t\t\t\"uploadContentList\": []base.Json{{\n\t\t\t\t\t\"contentName\": stream.GetName(),\n\t\t\t\t\t\"contentSize\": reportSize,\n\t\t\t\t\t// \"digest\": \"5a3231986ce7a6b46e408612d385bafa\"\n\t\t\t\t}},\n\t\t\t})\n\t\t\tpathname = \"/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL\"\n\t\t}\n\t\tvar resp UploadResp\n\t\tlog.Debugf(\"[139] upload request body: %+v\", data)\n\t\t_, err = d.post(pathname, data, &resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.Data.Result.ResultCode != \"0\" {\n\t\t\treturn fmt.Errorf(\"get file upload url failed with result code: %s, message: %s\", resp.Data.Result.ResultCode, resp.Data.Result.ResultDesc)\n\t\t}\n\n\t\tsize := stream.GetSize()\n\t\t// Progress\n\t\tp := driver.NewProgress(size, up)\n\t\tpartSize := d.getPartSize(size)\n\t\tpart := int64(1)\n\t\tif size > partSize {\n\t\t\tpart = (size + partSize - 1) / partSize\n\t\t}\n\t\trateLimited := driver.NewLimitedUploadStream(ctx, stream)\n\t\tfor i := int64(0); i < part; i++ {\n\t\t\tif utils.IsCanceled(ctx) {\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\n\t\t\tstart := i * partSize\n\t\t\tbyteSize := min(size-start, partSize)\n\n\t\t\tlimitReader := io.LimitReader(rateLimited, byteSize)\n\t\t\t// Update Progress\n\t\t\tr := io.TeeReader(limitReader, p)\n\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, r)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"text/plain;name=\"+unicode(stream.GetName()))\n\t\t\treq.Header.Set(\"contentSize\", strconv.FormatInt(size, 10))\n\t\t\treq.Header.Set(\"range\", fmt.Sprintf(\"bytes=%d-%d\", start, start+byteSize-1))\n\t\t\treq.Header.Set(\"uploadtaskID\", resp.Data.UploadResult.UploadTaskID)\n\t\t\treq.Header.Set(\"rangeType\", \"0\")\n\t\t\treq.ContentLength = byteSize\n\n\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif res.StatusCode != http.StatusOK {\n\t\t\t\tres.Body.Close()\n\t\t\t\treturn fmt.Errorf(\"unexpected status code: %d\", res.StatusCode)\n\t\t\t}\n\t\t\tbodyBytes, err := io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error reading response body: %v\", err)\n\t\t\t}\n\t\t\tvar result InterLayerUploadResult\n\t\t\terr = xml.Unmarshal(bodyBytes, &result)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error parsing XML: %v\", err)\n\t\t\t}\n\t\t\tif result.ResultCode != 0 {\n\t\t\t\treturn fmt.Errorf(\"upload failed with result code: %d, message: %s\", result.ResultCode, result.Msg)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n}\n\nfunc (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tswitch d.Addition.Type {\n\tcase MetaPersonalNew:\n\t\tvar resp base.Json\n\t\tvar uri string\n\t\tdata := base.Json{\n\t\t\t\"category\": \"video\",\n\t\t\t\"fileId\":   args.Obj.GetID(),\n\t\t}\n\t\tswitch args.Method {\n\t\tcase \"video_preview\":\n\t\t\turi = \"/videoPreview/getPreviewInfo\"\n\t\tdefault:\n\t\t\treturn nil, errs.NotSupport\n\t\t}\n\t\t_, err := d.personalPost(uri, data, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn resp[\"data\"], nil\n\tdefault:\n\t\treturn nil, errs.NotImplement\n\t}\n}\n\nfunc (d *Yun139) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif d.UserDomainID == \"\" {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar total, used int64\n\tif d.isFamily() {\n\t\tdiskInfo, err := d.getFamilyDiskInfo(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttotalMb, err := strconv.ParseInt(diskInfo.Data.DiskSize, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed convert disk size into integer: %+v\", err)\n\t\t}\n\t\tusedMb, err := strconv.ParseInt(diskInfo.Data.UsedSize, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed convert used size into integer: %+v\", err)\n\t\t}\n\t\ttotal = totalMb * 1024 * 1024\n\t\tused = usedMb * 1024 * 1024\n\t} else {\n\t\tdiskInfo, err := d.getPersonalDiskInfo(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttotalMb, err := strconv.ParseInt(diskInfo.Data.DiskSize, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed convert disk size into integer: %+v\", err)\n\t\t}\n\t\tfreeMb, err := strconv.ParseInt(diskInfo.Data.FreeDiskSize, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed convert free size into integer: %+v\", err)\n\t\t}\n\t\ttotal = totalMb * 1024 * 1024\n\t\tused = total - (freeMb * 1024 * 1024)\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Yun139)(nil)\n"
  },
  {
    "path": "drivers/139/meta.go",
    "content": "package _139\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t//Account       string `json:\"account\" required:\"true\"`\n\tAuthorization string `json:\"authorization\" type:\"text\" required:\"true\"`\n\tUsername      string `json:\"username\" required:\"true\"`\n\tPassword      string `json:\"password\" required:\"true\" secret:\"true\"`\n\tMailCookies   string `json:\"mail_cookies\" required:\"true\" type:\"text\" help:\"Cookies from mail.139.com used for login authentication.\"`\n\tdriver.RootID\n\tType                 string `json:\"type\" type:\"select\" options:\"personal_new,family,group,personal\" default:\"personal_new\"`\n\tCloudID              string `json:\"cloud_id\"`\n\tUserDomainID         string `json:\"user_domain_id\" help:\"ud_id in Cookie, fill in to show disk usage\"`\n\tCustomUploadPartSize int64  `json:\"custom_upload_part_size\" type:\"number\" default:\"0\" help:\"0 for auto\"`\n\tReportRealSize       bool   `json:\"report_real_size\" type:\"bool\" default:\"true\" help:\"Enable to report the real file size during upload\"`\n\tUseLargeThumbnail    bool   `json:\"use_large_thumbnail\" type:\"bool\" default:\"false\" help:\"Enable to use large thumbnail for images\"`\n}\n\nvar config = driver.Config{\n\tName:             \"139Yun\",\n\tLocalSort:        true,\n\tProxyRangeOption: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\td := &Yun139{}\n\t\td.ProxyRange = true\n\t\treturn d\n\t})\n}\n"
  },
  {
    "path": "drivers/139/types.go",
    "content": "package _139\n\nimport (\n\t\"encoding/xml\"\n)\n\nconst (\n\tMetaPersonal    string = \"personal\"\n\tMetaFamily      string = \"family\"\n\tMetaGroup       string = \"group\"\n\tMetaPersonalNew string = \"personal_new\"\n)\n\ntype BaseResp struct {\n\tSuccess bool   `json:\"success\"`\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Catalog struct {\n\tCatalogID   string `json:\"catalogID\"`\n\tCatalogName string `json:\"catalogName\"`\n\t//CatalogType     int         `json:\"catalogType\"`\n\tCreateTime string `json:\"createTime\"`\n\tUpdateTime string `json:\"updateTime\"`\n\t//IsShared        bool        `json:\"isShared\"`\n\t//CatalogLevel    int         `json:\"catalogLevel\"`\n\t//ShareDoneeCount int         `json:\"shareDoneeCount\"`\n\t//OpenType        int         `json:\"openType\"`\n\t//ParentCatalogID string      `json:\"parentCatalogId\"`\n\t//DirEtag         int         `json:\"dirEtag\"`\n\t//Tombstoned      int         `json:\"tombstoned\"`\n\t//ProxyID         interface{} `json:\"proxyID\"`\n\t//Moved           int         `json:\"moved\"`\n\t//IsFixedDir      int         `json:\"isFixedDir\"`\n\t//IsSynced        interface{} `json:\"isSynced\"`\n\t//Owner           string      `json:\"owner\"`\n\t//Modifier        interface{} `json:\"modifier\"`\n\t//Path            string      `json:\"path\"`\n\t//ShareType       int         `json:\"shareType\"`\n\t//SoftLink        interface{} `json:\"softLink\"`\n\t//ExtProp1        interface{} `json:\"extProp1\"`\n\t//ExtProp2        interface{} `json:\"extProp2\"`\n\t//ExtProp3        interface{} `json:\"extProp3\"`\n\t//ExtProp4        interface{} `json:\"extProp4\"`\n\t//ExtProp5        interface{} `json:\"extProp5\"`\n\t//ETagOprType     int         `json:\"ETagOprType\"`\n}\n\ntype Content struct {\n\tContentID   string `json:\"contentID\"`\n\tContentName string `json:\"contentName\"`\n\t//ContentSuffix   string      `json:\"contentSuffix\"`\n\tContentSize int64 `json:\"contentSize\"`\n\t//ContentDesc     string      `json:\"contentDesc\"`\n\t//ContentType     int         `json:\"contentType\"`\n\t//ContentOrigin   int         `json:\"contentOrigin\"`\n\tCreateTime string `json:\"createTime\"`\n\tUpdateTime string `json:\"updateTime\"`\n\t//CommentCount    int         `json:\"commentCount\"`\n\tThumbnailURL string `json:\"thumbnailURL\"`\n\t//BigthumbnailURL string      `json:\"bigthumbnailURL\"`\n\t//PresentURL      string      `json:\"presentURL\"`\n\t//PresentLURL     string      `json:\"presentLURL\"`\n\t//PresentHURL     string      `json:\"presentHURL\"`\n\t//ContentTAGList  interface{} `json:\"contentTAGList\"`\n\t//ShareDoneeCount int         `json:\"shareDoneeCount\"`\n\t//Safestate       int         `json:\"safestate\"`\n\t//Transferstate   int         `json:\"transferstate\"`\n\t//IsFocusContent  int         `json:\"isFocusContent\"`\n\t//UpdateShareTime interface{} `json:\"updateShareTime\"`\n\t//UploadTime      string      `json:\"uploadTime\"`\n\t//OpenType        int         `json:\"openType\"`\n\t//AuditResult     int         `json:\"auditResult\"`\n\t//ParentCatalogID string      `json:\"parentCatalogId\"`\n\t//Channel         string      `json:\"channel\"`\n\t//GeoLocFlag      string      `json:\"geoLocFlag\"`\n\tDigest string `json:\"digest\"`\n\t//Version         string      `json:\"version\"`\n\t//FileEtag        string      `json:\"fileEtag\"`\n\t//FileVersion     string      `json:\"fileVersion\"`\n\t//Tombstoned      int         `json:\"tombstoned\"`\n\t//ProxyID         string      `json:\"proxyID\"`\n\t//Moved           int         `json:\"moved\"`\n\t//MidthumbnailURL string      `json:\"midthumbnailURL\"`\n\t//Owner           string      `json:\"owner\"`\n\t//Modifier        string      `json:\"modifier\"`\n\t//ShareType       int         `json:\"shareType\"`\n\t//ExtInfo         struct {\n\t//\tUploader string `json:\"uploader\"`\n\t//\tAddress  string `json:\"address\"`\n\t//} `json:\"extInfo\"`\n\t//Exif struct {\n\t//\tCreateTime    string      `json:\"createTime\"`\n\t//\tLongitude     interface{} `json:\"longitude\"`\n\t//\tLatitude      interface{} `json:\"latitude\"`\n\t//\tLocalSaveTime interface{} `json:\"localSaveTime\"`\n\t//} `json:\"exif\"`\n\t//CollectionFlag interface{} `json:\"collectionFlag\"`\n\t//TreeInfo       interface{} `json:\"treeInfo\"`\n\t//IsShared       bool        `json:\"isShared\"`\n\t//ETagOprType    int         `json:\"ETagOprType\"`\n}\n\ntype GetDiskResp struct {\n\tBaseResp\n\tData struct {\n\t\tResult struct {\n\t\t\tResultCode string      `json:\"resultCode\"`\n\t\t\tResultDesc interface{} `json:\"resultDesc\"`\n\t\t} `json:\"result\"`\n\t\tGetDiskResult struct {\n\t\t\tParentCatalogID string    `json:\"parentCatalogID\"`\n\t\t\tNodeCount       int       `json:\"nodeCount\"`\n\t\t\tCatalogList     []Catalog `json:\"catalogList\"`\n\t\t\tContentList     []Content `json:\"contentList\"`\n\t\t\tIsCompleted     int       `json:\"isCompleted\"`\n\t\t} `json:\"getDiskResult\"`\n\t} `json:\"data\"`\n}\n\ntype UploadResp struct {\n\tBaseResp\n\tData struct {\n\t\tResult struct {\n\t\t\tResultCode string      `json:\"resultCode\"`\n\t\t\tResultDesc interface{} `json:\"resultDesc\"`\n\t\t} `json:\"result\"`\n\t\tUploadResult struct {\n\t\t\tUploadTaskID     string `json:\"uploadTaskID\"`\n\t\t\tRedirectionURL   string `json:\"redirectionUrl\"`\n\t\t\tNewContentIDList []struct {\n\t\t\t\tContentID     string `json:\"contentID\"`\n\t\t\t\tContentName   string `json:\"contentName\"`\n\t\t\t\tIsNeedUpload  string `json:\"isNeedUpload\"`\n\t\t\t\tFileEtag      int64  `json:\"fileEtag\"`\n\t\t\t\tFileVersion   int64  `json:\"fileVersion\"`\n\t\t\t\tOverridenFlag int    `json:\"overridenFlag\"`\n\t\t\t} `json:\"newContentIDList\"`\n\t\t\tCatalogIDList interface{} `json:\"catalogIDList\"`\n\t\t\tIsSlice       interface{} `json:\"isSlice\"`\n\t\t} `json:\"uploadResult\"`\n\t} `json:\"data\"`\n}\n\ntype InterLayerUploadResult struct {\n\tXMLName    xml.Name `xml:\"result\"`\n\tText       string   `xml:\",chardata\"`\n\tResultCode int      `xml:\"resultCode\"`\n\tMsg        string   `xml:\"msg\"`\n}\n\ntype CloudContent struct {\n\tContentID string `json:\"contentID\"`\n\t//Modifier         string      `json:\"modifier\"`\n\t//Nickname         string      `json:\"nickname\"`\n\t//CloudNickName    string      `json:\"cloudNickName\"`\n\tContentName string `json:\"contentName\"`\n\t//ContentType      int         `json:\"contentType\"`\n\t//ContentSuffix    string      `json:\"contentSuffix\"`\n\tContentSize int64 `json:\"contentSize\"`\n\t//ContentDesc      string      `json:\"contentDesc\"`\n\tCreateTime string `json:\"createTime\"`\n\t//Shottime         interface{} `json:\"shottime\"`\n\tLastUpdateTime string `json:\"lastUpdateTime\"`\n\tThumbnailURL   string `json:\"thumbnailURL\"`\n\t//MidthumbnailURL  string      `json:\"midthumbnailURL\"`\n\t//BigthumbnailURL  string      `json:\"bigthumbnailURL\"`\n\t//PresentURL       string      `json:\"presentURL\"`\n\t//PresentLURL      string      `json:\"presentLURL\"`\n\t//PresentHURL      string      `json:\"presentHURL\"`\n\t//ParentCatalogID  string      `json:\"parentCatalogID\"`\n\t//Uploader         string      `json:\"uploader\"`\n\t//UploaderNickName string      `json:\"uploaderNickName\"`\n\t//TreeInfo         interface{} `json:\"treeInfo\"`\n\t//UpdateTime       interface{} `json:\"updateTime\"`\n\t//ExtInfo          struct {\n\t//\tUploader string `json:\"uploader\"`\n\t//} `json:\"extInfo\"`\n\t//EtagOprType interface{} `json:\"etagOprType\"`\n}\n\ntype CloudCatalog struct {\n\tCatalogID   string `json:\"catalogID\"`\n\tCatalogName string `json:\"catalogName\"`\n\t//CloudID         string `json:\"cloudID\"`\n\tCreateTime     string `json:\"createTime\"`\n\tLastUpdateTime string `json:\"lastUpdateTime\"`\n\t//Creator         string `json:\"creator\"`\n\t//CreatorNickname string `json:\"creatorNickname\"`\n}\n\ntype QueryContentListResp struct {\n\tBaseResp\n\tData struct {\n\t\tResult struct {\n\t\t\tResultCode string `json:\"resultCode\"`\n\t\t\tResultDesc string `json:\"resultDesc\"`\n\t\t} `json:\"result\"`\n\t\tPath             string         `json:\"path\"`\n\t\tCloudContentList []CloudContent `json:\"cloudContentList\"`\n\t\tCloudCatalogList []CloudCatalog `json:\"cloudCatalogList\"`\n\t\tTotalCount       int            `json:\"totalCount\"`\n\t\tRecallContent    interface{}    `json:\"recallContent\"`\n\t} `json:\"data\"`\n}\n\ntype QueryGroupContentListResp struct {\n\tBaseResp\n\tData struct {\n\t\tResult struct {\n\t\t\tResultCode string `json:\"resultCode\"`\n\t\t\tResultDesc string `json:\"resultDesc\"`\n\t\t} `json:\"result\"`\n\t\tGetGroupContentResult struct {\n\t\t\tParentCatalogID string `json:\"parentCatalogID\"` // 根目录是\"0\"\n\t\t\tCatalogList     []struct {\n\t\t\t\tCatalog\n\t\t\t\tPath string `json:\"path\"`\n\t\t\t} `json:\"catalogList\"`\n\t\t\tContentList []Content `json:\"contentList\"`\n\t\t\tNodeCount   int       `json:\"nodeCount\"` // 文件+文件夹数量\n\t\t\tCtlgCnt     int       `json:\"ctlgCnt\"`   // 文件夹数量\n\t\t\tContCnt     int       `json:\"contCnt\"`   // 文件数量\n\t\t} `json:\"getGroupContentResult\"`\n\t} `json:\"data\"`\n}\n\ntype ParallelHashCtx struct {\n\tPartOffset int64 `json:\"partOffset\"`\n}\n\ntype PartInfo struct {\n\tPartNumber      int64           `json:\"partNumber\"`\n\tPartSize        int64           `json:\"partSize\"`\n\tParallelHashCtx ParallelHashCtx `json:\"parallelHashCtx\"`\n}\n\ntype PersonalThumbnail struct {\n\tStyle string `json:\"style\"`\n\tUrl   string `json:\"url\"`\n}\n\ntype PersonalFileItem struct {\n\tFileId     string              `json:\"fileId\"`\n\tName       string              `json:\"name\"`\n\tSize       int64               `json:\"size\"`\n\tType       string              `json:\"type\"`\n\tCreatedAt  string              `json:\"createdAt\"`\n\tUpdatedAt  string              `json:\"updatedAt\"`\n\tThumbnails []PersonalThumbnail `json:\"thumbnailUrls\"`\n}\n\ntype PersonalListResp struct {\n\tBaseResp\n\tData struct {\n\t\tItems          []PersonalFileItem `json:\"items\"`\n\t\tNextPageCursor string             `json:\"nextPageCursor\"`\n\t}\n}\n\ntype PersonalPartInfo struct {\n\tPartNumber int    `json:\"partNumber\"`\n\tUploadUrl  string `json:\"uploadUrl\"`\n}\n\ntype PersonalUploadResp struct {\n\tBaseResp\n\tData struct {\n\t\tFileId      string             `json:\"fileId\"`\n\t\tFileName    string             `json:\"fileName\"`\n\t\tPartInfos   []PersonalPartInfo `json:\"partInfos\"`\n\t\tExist       bool               `json:\"exist\"`\n\t\tRapidUpload bool               `json:\"rapidUpload\"`\n\t\tUploadId    string             `json:\"uploadId\"`\n\t}\n}\n\ntype PersonalUploadUrlResp struct {\n\tBaseResp\n\tData struct {\n\t\tFileId    string             `json:\"fileId\"`\n\t\tUploadId  string             `json:\"uploadId\"`\n\t\tPartInfos []PersonalPartInfo `json:\"partInfos\"`\n\t}\n}\n\ntype QueryRoutePolicyResp struct {\n\tSuccess bool   `json:\"success\"`\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tRoutePolicyList []struct {\n\t\t\tSiteID      string `json:\"siteID\"`\n\t\t\tSiteCode    string `json:\"siteCode\"`\n\t\t\tModName     string `json:\"modName\"`\n\t\t\tHttpUrl     string `json:\"httpUrl\"`\n\t\t\tHttpsUrl    string `json:\"httpsUrl\"`\n\t\t\tEnvID       string `json:\"envID\"`\n\t\t\tExtInfo     string `json:\"extInfo\"`\n\t\t\tHashName    string `json:\"hashName\"`\n\t\t\tModAddrType int    `json:\"modAddrType\"`\n\t\t} `json:\"routePolicyList\"`\n\t} `json:\"data\"`\n}\n\ntype RefreshTokenResp struct {\n\tXMLName     xml.Name `xml:\"root\"`\n\tReturn      string   `xml:\"return\"`\n\tToken       string   `xml:\"token\"`\n\tExpiretime  int32    `xml:\"expiretime\"`\n\tAccessToken string   `xml:\"accessToken\"`\n\tDesc        string   `xml:\"desc\"`\n}\n\ntype PersonalDiskInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tFreeDiskSize         string `json:\"freeDiskSize\"`\n\t\tDiskSize             string `json:\"diskSize\"`\n\t\tIsInfinitePicStorage *bool  `json:\"isInfinitePicStorage\"`\n\t} `json:\"data\"`\n}\n\ntype FamilyDiskInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tUsedSize string `json:\"usedSize\"`\n\t\tDiskSize string `json:\"diskSize\"`\n\t} `json:\"data\"`\n}\n\ntype AndAlbumUploadResp struct {\n\tResult struct {\n\t\tResultCode string `json:\"resultCode\"`\n\t\tResultDesc string `json:\"resultDesc\"`\n\t} `json:\"result\"`\n\tUploadResult struct {\n\t\tUploadTaskID     string `json:\"uploadTaskID\"`\n\t\tRedirectionURL   string `json:\"redirectionUrl\"`\n\t\tNewContentIDList []struct {\n\t\t\tContentID   string `json:\"contentID\"`\n\t\t\tContentName string `json:\"contentName\"`\n\t\t} `json:\"newContentIDList\"`\n\t} `json:\"uploadResult\"`\n}\n\ntype ModifyCloudDocV2Req struct {\n\tCatalogType       int    `json:\"catalogType\"`\n\tCloudID           string `json:\"cloudID\"`\n\tCommonAccountInfo struct {\n\t\tAccount     string `json:\"account\"`\n\t\tAccountType string `json:\"accountType\"`\n\t} `json:\"commonAccountInfo\"`\n\tDocLibName   string `json:\"docLibName\"`\n\tDocLibraryID string `json:\"docLibraryID\"`\n\tPath         string `json:\"path\"`\n}\n\ntype ModifyCloudDocV2Resp struct {\n\tResult struct {\n\t\tResultCode string `json:\"resultCode\"`\n\t\tResultDesc string `json:\"resultDesc\"`\n\t} `json:\"result\"`\n}\n\ntype CreateBatchOprTaskReq struct {\n\tCatalogList       []string `json:\"catalogList\"`\n\tCommonAccountInfo struct {\n\t\tAccount     string `json:\"account\"`\n\t\tAccountType string `json:\"accountType\"`\n\t} `json:\"commonAccountInfo\"`\n\tContentList       []string `json:\"contentList\"`\n\tDestCatalogID     string   `json:\"destCatalogID\"`\n\tDestGroupID       string   `json:\"destGroupID\"`\n\tDestPath          string   `json:\"destPath\"`\n\tDestType          int      `json:\"destType\"`\n\tSourceCatalogType int      `json:\"sourceCatalogType\"`\n\tSourceCloudID     string   `json:\"sourceCloudID\"`\n\tSourceType        int      `json:\"sourceType\"`\n\tTaskType          int      `json:\"taskType\"`\n}\n\ntype CreateBatchOprTaskResp struct {\n\tResult struct {\n\t\tResultCode string `json:\"resultCode\"`\n\t\tResultDesc string `json:\"resultDesc\"`\n\t} `json:\"result\"`\n\tTaskID string `json:\"taskID\"`\n}\n"
  },
  {
    "path": "drivers/139/util.go",
    "content": "package _139\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/md5\"\n\tcrypto_rand \"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tKEY_HEX_1 = \"73634235495062495331515373756c734e7253306c673d3d\" // 第一层 AES 解密密钥\n\tKEY_HEX_2 = \"7150714477323633586746674c337538\"                 // 第二层 AES 解密密钥\n)\n\n// do others that not defined in Driver interface\nfunc (d *Yun139) isFamily() bool {\n\treturn d.Type == \"family\"\n}\n\nfunc encodeURIComponent(str string) string {\n\tr := url.QueryEscape(str)\n\tr = strings.Replace(r, \"+\", \"%20\", -1)\n\tr = strings.Replace(r, \"%21\", \"!\", -1)\n\tr = strings.Replace(r, \"%27\", \"'\", -1)\n\tr = strings.Replace(r, \"%28\", \"(\", -1)\n\tr = strings.Replace(r, \"%29\", \")\", -1)\n\tr = strings.Replace(r, \"%2A\", \"*\", -1)\n\treturn r\n}\n\nfunc calSign(body, ts, randStr string) string {\n\tbody = encodeURIComponent(body)\n\tstrs := strings.Split(body, \"\")\n\tsort.Strings(strs)\n\tbody = strings.Join(strs, \"\")\n\tbody = base64.StdEncoding.EncodeToString([]byte(body))\n\tres := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+\":\"+randStr)\n\tres = strings.ToUpper(utils.GetMD5EncodeStr(res))\n\treturn res\n}\n\nfunc getTime(t string) time.Time {\n\tstamp, _ := time.ParseInLocation(\"20060102150405\", t, utils.CNLoc)\n\treturn stamp\n}\n\nfunc (d *Yun139) refreshToken() error {\n\tif d.ref != nil {\n\t\treturn d.ref.refreshToken()\n\t}\n\tdecode, err := base64.StdEncoding.DecodeString(d.Authorization)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"authorization decode failed: %s\", err)\n\t}\n\tdecodeStr := string(decode)\n\tsplits := strings.Split(decodeStr, \":\")\n\tif len(splits) < 3 {\n\t\treturn fmt.Errorf(\"authorization is invalid, splits < 3\")\n\t}\n\td.Account = splits[1]\n\tstrs := strings.Split(splits[2], \"|\")\n\tif len(strs) < 4 {\n\t\treturn fmt.Errorf(\"authorization is invalid, strs < 4\")\n\t}\n\texpiration, err := strconv.ParseInt(strs[3], 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"authorization is invalid\")\n\t}\n\texpiration -= time.Now().UnixMilli()\n\tif expiration > 1000*60*60*24*15 {\n\t\t// Authorization有效期大于15天无需刷新\n\t\treturn nil\n\t}\n\tif expiration < 0 {\n\t\treturn fmt.Errorf(\"authorization has expired\")\n\t}\n\n\turl := \"https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do\"\n\tvar resp RefreshTokenResp\n\treqBody := \"<root><token>\" + splits[2] + \"</token><account>\" + splits[1] + \"</account><clienttype>656</clienttype></root>\"\n\t_, err = base.RestyClient.R().\n\t\tForceContentType(\"application/xml\").\n\t\tSetBody(reqBody).\n\t\tSetResult(&resp).\n\t\tPost(url)\n\tif err != nil || resp.Return != \"0\" {\n\t\tlog.Warnf(\"139yun: failed to refresh token with old token: %v, desc: %s. trying to login with password.\", err, resp.Desc)\n\t\tnewAuth, loginErr := d.loginWithPassword()\n\t\tlog.Debugf(\"newAuth: Ok: %s\", newAuth)\n\t\tif loginErr != nil {\n\t\t\treturn fmt.Errorf(\"failed to login with password after refresh failed: %w\", loginErr)\n\t\t}\n\t\treturn nil\n\t}\n\n\td.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + \":\" + splits[1] + \":\" + resp.Token))\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Yun139) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\trandStr := random.String(16)\n\tts := time.Now().Format(\"2006-01-02 15:04:05\")\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tbody, err := utils.Json.Marshal(req.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsign := calSign(string(body), ts, randStr)\n\tsvcType := \"1\"\n\tif d.isFamily() {\n\t\tsvcType = \"2\"\n\t}\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":         \"application/json, text/plain, */*\",\n\t\t\"CMS-DEVICE\":     \"default\",\n\t\t\"Authorization\":  \"Basic \" + d.getAuthorization(),\n\t\t\"mcloud-channel\": \"1000101\",\n\t\t\"mcloud-client\":  \"10701\",\n\t\t//\"mcloud-route\": \"001\",\n\t\t\"mcloud-sign\": fmt.Sprintf(\"%s,%s,%s\", ts, randStr, sign),\n\t\t//\"mcloud-skey\":\"\",\n\t\t\"mcloud-version\":         \"7.14.0\",\n\t\t\"Origin\":                 \"https://yun.139.com\",\n\t\t\"Referer\":                \"https://yun.139.com/w/\",\n\t\t\"x-DeviceInfo\":           \"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||\",\n\t\t\"x-huawei-channelSrc\":    \"10000034\",\n\t\t\"x-inner-ntwk\":           \"2\",\n\t\t\"x-m4c-caller\":           \"PC\",\n\t\t\"x-m4c-src\":              \"10002\",\n\t\t\"x-SvcType\":              svcType,\n\t\t\"Inner-Hcy-Router-Https\": \"1\",\n\t})\n\n\tvar e BaseResp\n\treq.SetResult(&e)\n\tlog.Debugf(\"[139] request: %s %s, body: %s\", method, url, string(body))\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\tlog.Debugf(\"[139] request error: %v\", err)\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"[139] response body: %s\", res.String())\n\tif !e.Success {\n\t\t// Always try to unmarshal to the specific response type first if 'resp' is provided.\n\t\tif resp != nil {\n\t\t\terr = utils.Json.Unmarshal(res.Body(), resp)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"[139] failed to unmarshal response to specific type: %v\", err)\n\t\t\t\treturn nil, err // Return unmarshal error\n\t\t\t}\n\t\t\tif createBatchOprTaskResp, ok := resp.(*CreateBatchOprTaskResp); ok {\n\t\t\t\tlog.Debugf(\"[139] CreateBatchOprTaskResp.Result.ResultCode: %s\", createBatchOprTaskResp.Result.ResultCode)\n\t\t\t\tif createBatchOprTaskResp.Result.ResultCode == \"0\" {\n\t\t\t\t\tgoto SUCCESS_PROCESS\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil, errors.New(e.Message) // Fallback to original error if not handled\n\t}\n\tif resp != nil {\n\t\terr = utils.Json.Unmarshal(res.Body(), resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\nSUCCESS_PROCESS:\n\treturn res.Body(), nil\n}\n\nfunc (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error) {\n\turl := \"https://user-njs.yun.139.com/user/route/qryRoutePolicy\"\n\treq := base.RestyClient.R()\n\trandStr := random.String(16)\n\tts := time.Now().Format(\"2006-01-02 15:04:05\")\n\tcallback := func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tbody, err := utils.Json.Marshal(req.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsign := calSign(string(body), ts, randStr)\n\tsvcType := \"1\"\n\tif d.isFamily() {\n\t\tsvcType = \"2\"\n\t}\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":         \"application/json, text/plain, */*\",\n\t\t\"CMS-DEVICE\":     \"default\",\n\t\t\"Authorization\":  \"Basic \" + d.getAuthorization(),\n\t\t\"mcloud-channel\": \"1000101\",\n\t\t\"mcloud-client\":  \"10701\",\n\t\t//\"mcloud-route\": \"001\",\n\t\t\"mcloud-sign\": fmt.Sprintf(\"%s,%s,%s\", ts, randStr, sign),\n\t\t//\"mcloud-skey\":\"\",\n\t\t\"mcloud-version\":         \"7.14.0\",\n\t\t\"Origin\":                 \"https://yun.139.com\",\n\t\t\"Referer\":                \"https://yun.139.com/w/\",\n\t\t\"x-DeviceInfo\":           \"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||\",\n\t\t\"x-huawei-channelSrc\":    \"10000034\",\n\t\t\"x-inner-ntwk\":           \"2\",\n\t\t\"x-m4c-caller\":           \"PC\",\n\t\t\"x-m4c-src\":              \"10002\",\n\t\t\"x-SvcType\":              svcType,\n\t\t\"Inner-Hcy-Router-Https\": \"1\",\n\t})\n\n\tvar e BaseResp\n\treq.SetResult(&e)\n\tres, err := req.Execute(http.MethodPost, url)\n\tlog.Debugln(res.String())\n\tif !e.Success {\n\t\treturn nil, errors.New(e.Message)\n\t}\n\tif resp != nil {\n\t\terr = utils.Json.Unmarshal(res.Body(), resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {\n\treturn d.request(\"https://yun.139.com\"+pathname, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, resp)\n}\n\nfunc (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {\n\tstart := 0\n\tlimit := 100\n\tfiles := make([]model.Obj, 0)\n\tfor {\n\t\tdata := base.Json{\n\t\t\t\"catalogID\":       catalogID,\n\t\t\t\"sortDirection\":   1,\n\t\t\t\"startNumber\":     start + 1,\n\t\t\t\"endNumber\":       start + limit,\n\t\t\t\"filterType\":      0,\n\t\t\t\"catalogSortType\": 0,\n\t\t\t\"contentSortType\": 0,\n\t\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\t\"account\":     d.getAccount(),\n\t\t\t\t\"accountType\": 1,\n\t\t\t},\n\t\t}\n\t\tvar resp GetDiskResp\n\t\t_, err := d.post(\"/orchestration/personalCloud/catalog/v1.0/getDisk\", data, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, catalog := range resp.Data.GetDiskResult.CatalogList {\n\t\t\tf := model.Object{\n\t\t\t\tID:       catalog.CatalogID,\n\t\t\t\tName:     catalog.CatalogName,\n\t\t\t\tSize:     0,\n\t\t\t\tModified: getTime(catalog.UpdateTime),\n\t\t\t\tCtime:    getTime(catalog.CreateTime),\n\t\t\t\tIsFolder: true,\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tfor _, content := range resp.Data.GetDiskResult.ContentList {\n\t\t\tf := model.ObjThumb{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       content.ContentID,\n\t\t\t\t\tName:     content.ContentName,\n\t\t\t\t\tSize:     content.ContentSize,\n\t\t\t\t\tModified: getTime(content.UpdateTime),\n\t\t\t\t\tHashInfo: utils.NewHashInfo(utils.MD5, content.Digest),\n\t\t\t\t},\n\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},\n\t\t\t\t// Thumbnail: content.BigthumbnailURL,\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tif start+limit >= resp.Data.GetDiskResult.NodeCount {\n\t\t\tbreak\n\t\t}\n\t\tstart += limit\n\t}\n\treturn files, nil\n}\n\nfunc (d *Yun139) newJson(data map[string]interface{}) base.Json {\n\tcommon := map[string]interface{}{\n\t\t\"catalogType\": 3,\n\t\t\"cloudID\":     d.CloudID,\n\t\t\"cloudType\":   1,\n\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\"account\":     d.getAccount(),\n\t\t\t\"accountType\": 1,\n\t\t},\n\t}\n\treturn utils.MergeMap(data, common)\n}\n\nfunc (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {\n\tpageNum := 1\n\tfiles := make([]model.Obj, 0)\n\tfor {\n\t\tdata := d.newJson(base.Json{\n\t\t\t\"catalogID\":       catalogID,\n\t\t\t\"contentSortType\": 0,\n\t\t\t\"pageInfo\": base.Json{\n\t\t\t\t\"pageNum\":  pageNum,\n\t\t\t\t\"pageSize\": 100,\n\t\t\t},\n\t\t\t\"sortDirection\": 1,\n\t\t})\n\t\tvar resp QueryContentListResp\n\t\t_, err := d.post(\"/orchestration/familyCloud-rebuild/content/v1.2/queryContentList\", data, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpath := resp.Data.Path\n\t\tif catalogID == d.RootFolderID {\n\t\t\td.RootPath = path\n\t\t}\n\t\tfor _, catalog := range resp.Data.CloudCatalogList {\n\t\t\tf := model.Object{\n\t\t\t\tID:       catalog.CatalogID,\n\t\t\t\tName:     catalog.CatalogName,\n\t\t\t\tSize:     0,\n\t\t\t\tIsFolder: true,\n\t\t\t\tModified: getTime(catalog.LastUpdateTime),\n\t\t\t\tCtime:    getTime(catalog.CreateTime),\n\t\t\t\tPath:     path, // 文件夹上一级的Path\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tfor _, content := range resp.Data.CloudContentList {\n\t\t\tf := model.ObjThumb{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       content.ContentID,\n\t\t\t\t\tName:     content.ContentName,\n\t\t\t\t\tSize:     content.ContentSize,\n\t\t\t\t\tModified: getTime(content.LastUpdateTime),\n\t\t\t\t\tCtime:    getTime(content.CreateTime),\n\t\t\t\t\tPath:     path, // 文件所在目录的Path\n\t\t\t\t},\n\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},\n\t\t\t\t// Thumbnail: content.BigthumbnailURL,\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tif resp.Data.TotalCount == 0 {\n\t\t\tbreak\n\t\t}\n\t\tpageNum++\n\t}\n\treturn files, nil\n}\n\nfunc (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {\n\tpageNum := 1\n\tfiles := make([]model.Obj, 0)\n\tfor {\n\t\tdata := d.newJson(base.Json{\n\t\t\t\"groupID\":         d.CloudID,\n\t\t\t\"catalogID\":       path.Base(catalogID),\n\t\t\t\"contentSortType\": 0,\n\t\t\t\"sortDirection\":   1,\n\t\t\t\"startNumber\":     pageNum,\n\t\t\t\"endNumber\":       pageNum + 99,\n\t\t\t\"path\":            path.Join(d.RootFolderID, catalogID),\n\t\t})\n\n\t\tvar resp QueryGroupContentListResp\n\t\t_, err := d.post(\"/orchestration/group-rebuild/content/v1.0/queryGroupContentList\", data, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpath := resp.Data.GetGroupContentResult.ParentCatalogID\n\t\tif catalogID == d.RootFolderID {\n\t\t\td.RootPath = path\n\t\t}\n\t\tfor _, catalog := range resp.Data.GetGroupContentResult.CatalogList {\n\t\t\tf := model.Object{\n\t\t\t\tID:       catalog.CatalogID,\n\t\t\t\tName:     catalog.CatalogName,\n\t\t\t\tSize:     0,\n\t\t\t\tIsFolder: true,\n\t\t\t\tModified: getTime(catalog.UpdateTime),\n\t\t\t\tCtime:    getTime(catalog.CreateTime),\n\t\t\t\tPath:     catalog.Path, // 文件夹的真实Path， root:/开头\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tfor _, content := range resp.Data.GetGroupContentResult.ContentList {\n\t\t\tf := model.ObjThumb{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       content.ContentID,\n\t\t\t\t\tName:     content.ContentName,\n\t\t\t\t\tSize:     content.ContentSize,\n\t\t\t\t\tModified: getTime(content.UpdateTime),\n\t\t\t\t\tCtime:    getTime(content.CreateTime),\n\t\t\t\t\tPath:     path, // 文件所在目录的Path\n\t\t\t\t},\n\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},\n\t\t\t\t// Thumbnail: content.BigthumbnailURL,\n\t\t\t}\n\t\t\tfiles = append(files, &f)\n\t\t}\n\t\tif (pageNum + 99) > resp.Data.GetGroupContentResult.NodeCount {\n\t\t\tbreak\n\t\t}\n\t\tpageNum = pageNum + 100\n\t}\n\treturn files, nil\n}\n\nfunc (d *Yun139) getLink(contentId string) (string, error) {\n\tdata := base.Json{\n\t\t\"appName\":   \"\",\n\t\t\"contentID\": contentId,\n\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\"account\":     d.getAccount(),\n\t\t\t\"accountType\": 1,\n\t\t},\n\t}\n\tres, err := d.post(\"/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest\",\n\t\tdata, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn jsoniter.Get(res, \"data\", \"downloadURL\").ToString(), nil\n}\n\nfunc (d *Yun139) familyGetLink(contentId string, path string) (string, error) {\n\tdata := d.newJson(base.Json{\n\t\t\"contentID\": contentId,\n\t\t\"path\":      path,\n\t})\n\tres, err := d.post(\"/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL\",\n\t\tdata, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn jsoniter.Get(res, \"data\", \"downloadURL\").ToString(), nil\n}\n\nfunc (d *Yun139) groupGetLink(contentId string, path string) (string, error) {\n\tdata := d.newJson(base.Json{\n\t\t\"contentID\": contentId,\n\t\t\"groupID\":   d.CloudID,\n\t\t\"path\":      path,\n\t})\n\tres, err := d.post(\"/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL\",\n\t\tdata, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn jsoniter.Get(res, \"data\", \"downloadURL\").ToString(), nil\n}\n\nfunc unicode(str string) string {\n\ttextQuoted := strconv.QuoteToASCII(str)\n\ttextUnquoted := textQuoted[1 : len(textQuoted)-1]\n\treturn textUnquoted\n}\n\nfunc (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\turl := d.getPersonalCloudHost() + pathname\n\treq := base.RestyClient.R()\n\trandStr := random.String(16)\n\tts := time.Now().Format(\"2006-01-02 15:04:05\")\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tbody, err := utils.Json.Marshal(req.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsign := calSign(string(body), ts, randStr)\n\tsvcType := \"1\"\n\tif d.isFamily() {\n\t\tsvcType = \"2\"\n\t}\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":               \"application/json, text/plain, */*\",\n\t\t\"Authorization\":        \"Basic \" + d.getAuthorization(),\n\t\t\"Caller\":               \"web\",\n\t\t\"Cms-Device\":           \"default\",\n\t\t\"Mcloud-Channel\":       \"1000101\",\n\t\t\"Mcloud-Client\":        \"10701\",\n\t\t\"Mcloud-Route\":         \"001\",\n\t\t\"Mcloud-Sign\":          fmt.Sprintf(\"%s,%s,%s\", ts, randStr, sign),\n\t\t\"Mcloud-Version\":       \"7.14.0\",\n\t\t\"x-DeviceInfo\":         \"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||\",\n\t\t\"x-huawei-channelSrc\":  \"10000034\",\n\t\t\"x-inner-ntwk\":         \"2\",\n\t\t\"x-m4c-caller\":         \"PC\",\n\t\t\"x-m4c-src\":            \"10002\",\n\t\t\"x-SvcType\":            svcType,\n\t\t\"X-Yun-Api-Version\":    \"v1\",\n\t\t\"X-Yun-App-Channel\":    \"10000034\",\n\t\t\"X-Yun-Channel-Source\": \"10000034\",\n\t\t\"X-Yun-Client-Info\":    \"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||\",\n\t\t\"X-Yun-Module-Type\":    \"100\",\n\t\t\"X-Yun-Svc-Type\":       \"1\",\n\t})\n\n\tvar e BaseResp\n\treq.SetResult(&e)\n\tlog.Debugf(\"[139] personal request: %s %s, body: %s\", method, url, string(body))\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\tlog.Debugf(\"[139] personal request error: %v\", err)\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"[139] personal response body: %s\", res.String())\n\tif !e.Success {\n\t\treturn nil, errors.New(e.Message)\n\t}\n\tif resp != nil {\n\t\terr = utils.Json.Unmarshal(res.Body(), resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {\n\treturn d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, resp)\n}\n\nfunc (d *Yun139) isboPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {\n\turl := \"https://group.yun.139.com/hcy/mutual/adapter\" + pathname\n\treturn d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, resp)\n}\n\nfunc getPersonalTime(t string) time.Time {\n\tstamp, err := time.ParseInLocation(\"2006-01-02T15:04:05.999-07:00\", t, utils.CNLoc)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn stamp\n}\n\nfunc (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tnextPageCursor := \"\"\n\tfor {\n\t\tdata := base.Json{\n\t\t\t\"imageThumbnailStyleList\": []string{\"Small\", \"Large\"},\n\t\t\t\"orderBy\":                 \"updated_at\",\n\t\t\t\"orderDirection\":          \"DESC\",\n\t\t\t\"pageInfo\": base.Json{\n\t\t\t\t\"pageCursor\": nextPageCursor,\n\t\t\t\t\"pageSize\":   100,\n\t\t\t},\n\t\t\t\"parentFileId\": fileId,\n\t\t}\n\t\tvar resp PersonalListResp\n\t\t_, err := d.personalPost(\"/file/list\", data, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnextPageCursor = resp.Data.NextPageCursor\n\t\tfor _, item := range resp.Data.Items {\n\t\t\tisFolder := (item.Type == \"folder\")\n\t\t\tvar f model.Obj\n\t\t\tif isFolder {\n\t\t\t\tf = &model.Object{\n\t\t\t\t\tID:       item.FileId,\n\t\t\t\t\tName:     item.Name,\n\t\t\t\t\tSize:     0,\n\t\t\t\t\tModified: getPersonalTime(item.UpdatedAt),\n\t\t\t\t\tCtime:    getPersonalTime(item.CreatedAt),\n\t\t\t\t\tIsFolder: isFolder,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tThumbnails := item.Thumbnails\n\t\t\t\tvar ThumbnailUrl string\n\t\t\t\tif d.UseLargeThumbnail {\n\t\t\t\t\tfor _, thumb := range Thumbnails {\n\t\t\t\t\t\tif strings.Contains(thumb.Style, \"Large\") {\n\t\t\t\t\t\t\tThumbnailUrl = thumb.Url\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ThumbnailUrl == \"\" && len(Thumbnails) > 0 {\n\t\t\t\t\tThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url\n\t\t\t\t}\n\t\t\t\tf = &model.ObjThumb{\n\t\t\t\t\tObject: model.Object{\n\t\t\t\t\t\tID:       item.FileId,\n\t\t\t\t\t\tName:     item.Name,\n\t\t\t\t\t\tSize:     item.Size,\n\t\t\t\t\t\tModified: getPersonalTime(item.UpdatedAt),\n\t\t\t\t\t\tCtime:    getPersonalTime(item.CreatedAt),\n\t\t\t\t\t\tIsFolder: isFolder,\n\t\t\t\t\t},\n\t\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl},\n\t\t\t\t}\n\t\t\t}\n\t\t\tfiles = append(files, f)\n\t\t}\n\t\tif len(nextPageCursor) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (d *Yun139) personalGetLink(fileId string) (string, error) {\n\tdata := base.Json{\n\t\t\"fileId\": fileId,\n\t}\n\tres, err := d.personalPost(\"/file/getDownloadUrl\",\n\t\tdata, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcdnUrl := jsoniter.Get(res, \"data\", \"cdnUrl\").ToString()\n\tif cdnUrl != \"\" {\n\t\treturn cdnUrl, nil\n\t} else {\n\t\treturn jsoniter.Get(res, \"data\", \"url\").ToString(), nil\n\t}\n}\n\nfunc (d *Yun139) getAuthorization() string {\n\tif d.ref != nil {\n\t\treturn d.ref.getAuthorization()\n\t}\n\treturn d.Authorization\n}\n\nfunc (d *Yun139) getAccount() string {\n\tif d.ref != nil {\n\t\treturn d.ref.getAccount()\n\t}\n\treturn d.Account\n}\n\nfunc (d *Yun139) getPersonalCloudHost() string {\n\tif d.ref != nil {\n\t\treturn d.ref.getPersonalCloudHost()\n\t}\n\treturn d.PersonalCloudHost\n}\n\nfunc (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, uploadPartInfos []PersonalPartInfo, rateLimited *driver.RateLimitReader, p *driver.Progress) error {\n\t// 确保数组以 PartNumber 从小到大排序\n\tsort.Slice(uploadPartInfos, func(i, j int) bool {\n\t\treturn uploadPartInfos[i].PartNumber < uploadPartInfos[j].PartNumber\n\t})\n\n\tfor _, uploadPartInfo := range uploadPartInfos {\n\t\tindex := uploadPartInfo.PartNumber - 1\n\t\tif index < 0 || index >= len(partInfos) {\n\t\t\treturn fmt.Errorf(\"invalid PartNumber %d: index out of bounds (partInfos length: %d)\", uploadPartInfo.PartNumber, len(partInfos))\n\t\t}\n\t\tpartSize := partInfos[index].PartSize\n\t\tlog.Debugf(\"[139] uploading part %+v/%+v\", index, len(partInfos))\n\t\tlimitReader := io.LimitReader(rateLimited, partSize)\n\t\tr := io.TeeReader(limitReader, p)\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t\treq.Header.Set(\"Content-Length\", fmt.Sprint(partSize))\n\t\treq.Header.Set(\"Origin\", \"https://yun.139.com\")\n\t\treq.Header.Set(\"Referer\", \"https://yun.139.com/\")\n\t\treq.ContentLength = partSize\n\t\terr = func() error {\n\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer res.Body.Close()\n\t\t\tlog.Debugf(\"[139] uploaded: %+v\", res)\n\t\t\tif res.StatusCode != http.StatusOK {\n\t\t\t\tbody, _ := io.ReadAll(res.Body)\n\t\t\t\treturn fmt.Errorf(\"unexpected status code: %d, body: %s\", res.StatusCode, string(body))\n\t\t\t}\n\t\t\treturn nil\n\t\t}()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Yun139) getPersonalDiskInfo(ctx context.Context) (*PersonalDiskInfoResp, error) {\n\tdata := map[string]interface{}{\n\t\t\"userDomainId\": d.UserDomainID,\n\t}\n\tvar resp PersonalDiskInfoResp\n\t_, err := d.request(\"https://user-njs.yun.139.com/user/disk/getPersonalDiskInfo\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Yun139) getFamilyDiskInfo(ctx context.Context) (*FamilyDiskInfoResp, error) {\n\tdata := map[string]interface{}{\n\t\t\"userDomainId\": d.UserDomainID,\n\t}\n\tvar resp FamilyDiskInfoResp\n\t_, err := d.request(\"https://user-njs.yun.139.com/user/disk/getFamilyDiskInfo\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc getMd5(dataStr string) string {\n\thash := md5.Sum([]byte(dataStr))\n\treturn fmt.Sprintf(\"%x\", hash)\n}\n\nfunc (d *Yun139) step1_password_login() (string, error) {\n\tlog.Debugf(\"--- 执行步骤 1: 登录 API ---\")\n\tloginURL := \"https://mail.10086.cn/Login/Login.ashx\"\n\n\t// 密码 SHA1 哈希\n\thashedPassword := sha1Hash(fmt.Sprintf(\"fetion.com.cn:%s\", d.Password))\n\tlog.Debugf(\"DEBUG: 原始密码: %s\", d.Password)\n\tlog.Debugf(\"DEBUG: SHA1 输入: fetion.com.cn:%s\", d.Password)\n\tlog.Debugf(\"DEBUG: 生成的 Password 哈希: %s\", hashedPassword)\n\n\tcguid := strconv.FormatInt(time.Now().UnixMilli(), 10) // 随机生成 cguid\n\n\tloginHeaders := map[string]string{\n\t\t\"accept\":                    \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n\t\t\"accept-language\":           \"zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6,en-GB;q=0.5\",\n\t\t\"cache-control\":             \"max-age=0\",\n\t\t\"content-type\":              \"application/x-www-form-urlencoded\",\n\t\t\"dnt\":                       \"1\",\n\t\t\"origin\":                    \"https://mail.10086.cn\",\n\t\t\"priority\":                  \"u=0, i\",\n\t\t\"referer\":                   fmt.Sprintf(\"https://mail.10086.cn/default.html?&s=1&v=0&u=%s&m=1&ec=S001&resource=indexLogin&clientid=1003&auto=on&cguid=%s&mtime=45\", base64.StdEncoding.EncodeToString([]byte(d.Username)), cguid),\n\t\t\"sec-ch-ua\":                 \"\\\"Microsoft Edge\\\";v=\\\"141\\\", \\\"Not?A_Brand\\\";v=\\\"8\\\", \\\"Chromium\\\";v=\\\"141\\\"\",\n\t\t\"sec-ch-ua-mobile\":          \"?0\",\n\t\t\"sec-ch-ua-platform\":        \"\\\"Windows\\\"\",\n\t\t\"sec-fetch-dest\":            \"document\",\n\t\t\"sec-fetch-mode\":            \"navigate\",\n\t\t\"sec-fetch-site\":            \"same-origin\",\n\t\t\"sec-fetch-user\":            \"?1\",\n\t\t\"upgrade-insecure-requests\": \"1\",\n\t\t\"user-agent\":                \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0\",\n\t\t\"Cookie\":                    d.MailCookies,\n\t}\n\n\tloginData := url.Values{}\n\tloginData.Set(\"UserName\", d.Username)\n\tloginData.Set(\"passOld\", \"\")\n\tloginData.Set(\"auto\", \"on\")\n\tloginData.Set(\"Password\", hashedPassword)\n\tloginData.Set(\"webIndexPagePwdLogin\", \"1\")\n\tloginData.Set(\"pwdType\", \"1\")\n\tloginData.Set(\"clientId\", \"1003\")\n\tloginData.Set(\"authType\", \"2\")\n\n\tlog.Debugf(\"DEBUG: 登录请求 URL: %s\", loginURL)\n\tlog.Debugf(\"DEBUG: 登录请求 Headers: %+v\", loginHeaders)\n\tlog.Debugf(\"DEBUG: 登录请求 Body: %s\", loginData.Encode())\n\n\t// 设置客户端不跟随重定向\n\tclient := base.RestyClient.SetRedirectPolicy(resty.NoRedirectPolicy())\n\tres, err := client.R().\n\t\tSetHeaders(loginHeaders).\n\t\tSetFormDataFromValues(loginData).\n\t\tPost(loginURL)\n\n\tif err != nil {\n\t\t// 如果是重定向错误，则不作为失败处理，因为我们禁止了自动重定向\n\t\tif res != nil && res.StatusCode() >= 300 && res.StatusCode() < 400 {\n\t\t\tlog.Debugf(\"DEBUG: 登录响应 Status Code: %d (Redirect)\", res.StatusCode())\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"step1 login request failed: %w\", err)\n\t\t}\n\t} else {\n\t\tlog.Debugf(\"DEBUG: 登录响应 Status Code: %d\", res.StatusCode())\n\t}\n\t// 恢复客户端的默认重定向策略，以免影响后续请求\n\tbase.RestyClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))\n\tlog.Debugf(\"DEBUG: 登录响应 Headers: %+v\", res.Header())\n\n\tvar sid, extractedCguid string\n\n\t// 从 Location 头部提取 sid 和 cguid\n\tlocationHeader := res.Header().Get(\"Location\")\n\tif locationHeader != \"\" {\n\t\tsidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader)\n\t\tcguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader)\n\t\tif len(sidMatch) > 1 {\n\t\t\tsid = sidMatch[1]\n\t\t\tlog.Debugf(\"DEBUG: 从 Location 提取到 sid: %s\", sid)\n\t\t}\n\t\tif len(cguidMatch) > 1 {\n\t\t\textractedCguid = cguidMatch[1]\n\t\t\tlog.Debugf(\"DEBUG: 从 Location 提取到 cguid: %s\", extractedCguid)\n\t\t}\n\t}\n\n\t// 如果 Location 中没有，尝试从 Set-Cookie 中提取\n\tif sid == \"\" || extractedCguid == \"\" {\n\t\tsetCookieHeaders := res.Header().Values(\"Set-Cookie\")\n\t\tfor _, cookieStr := range setCookieHeaders {\n\t\t\tssoSidMatch := regexp.MustCompile(`Os_SSo_Sid=([^;]+)`).FindStringSubmatch(cookieStr)\n\t\t\tcookieCguidMatch := regexp.MustCompile(`cguid=([^;]+)`).FindStringSubmatch(cookieStr)\n\t\t\tif len(ssoSidMatch) > 1 && sid == \"\" {\n\t\t\t\tsid = ssoSidMatch[1]\n\t\t\t\tlog.Debugf(\"DEBUG: 从 Set-Cookie 提取到 sid: %s\", sid)\n\t\t\t}\n\t\t\tif len(cookieCguidMatch) > 1 && extractedCguid == \"\" {\n\t\t\t\textractedCguid = cookieCguidMatch[1]\n\t\t\t\tlog.Debugf(\"DEBUG: 从 Set-Cookie 提取到 cguid: %s\", extractedCguid)\n\t\t\t}\n\t\t}\n\t}\n\n\tif sid == \"\" || extractedCguid == \"\" {\n\t\treturn \"\", errors.New(\"failed to extract sid or cguid from login response\")\n\t}\n\n\t// 提取并记录 cookies\n\tloginUrlObj, _ := url.Parse(loginURL)\n\tcookies := base.RestyClient.GetClient().Jar.Cookies(loginUrlObj)\n\tvar cookieStrings []string\n\tfor _, cookie := range cookies {\n\t\tcookieStrings = append(cookieStrings, cookie.Name+\"=\"+cookie.Value)\n\t}\n\tcookieStr := strings.Join(cookieStrings, \"; \")\n\tlog.Debugf(\"DEBUG: 提取到的 Cookies: %s\", cookieStr)\n\td.MailCookies = cookieStr\n\n\treturn sid, nil\n}\n\nfunc (d *Yun139) step2_get_single_token(sid string) (string, error) {\n\tlog.Debugf(\"\\n--- 执行步骤 2: 换artifact API ---\")\n\tcguid := strconv.FormatInt(time.Now().UnixMilli(), 10)\n\n\texchangeArtifactURL := fmt.Sprintf(\"https://smsrebuild1.mail.10086.cn/setting/s?func=%s&sid=%s&cguid=%s\", url.QueryEscape(\"umc:getArtifact\"), sid, cguid)\n\n\t// 从 MailCookies 中提取 RMKEY\n\tvar rmkey string\n\tcookies := strings.Split(d.MailCookies, \";\")\n\tfor _, cookie := range cookies {\n\t\tcookie = strings.TrimSpace(cookie)\n\t\tif strings.HasPrefix(cookie, \"RMKEY=\") {\n\t\t\trmkey = cookie\n\t\t\tbreak\n\t\t}\n\t}\n\tif rmkey == \"\" {\n\t\treturn \"\", errors.New(\"RMKEY not found in MailCookies\")\n\t}\n\n\texchangePassidHeaders := map[string]string{\n\t\t\"Host\":            \"smsrebuild1.mail.10086.cn\",\n\t\t\"Cookie\":          rmkey,\n\t\t\"Content-Type\":    \"text/xml; charset=utf-8\",\n\t\t\"Accept-Encoding\": \"gzip\",\n\t\t\"User-Agent\":      \"okhttp/4.12.0\",\n\t}\n\n\tlog.Debugf(\"DEBUG: 换passid 请求 URL: %s\", exchangeArtifactURL)\n\tlog.Debugf(\"DEBUG: 换passid 请求 Headers: %+v\", exchangePassidHeaders)\n\n\tres, err := base.RestyClient.R().\n\t\tSetHeaders(exchangePassidHeaders).\n\t\tPost(exchangeArtifactURL)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"step2 exchange artifact request failed: %w\", err)\n\t}\n\n\tlog.Debugf(\"DEBUG: 换passid 响应 Status Code: %d\", res.StatusCode())\n\tlog.Debugf(\"DEBUG: 换passid 响应 Headers: %+v\", res.Header())\n\tlog.Debugf(\"DEBUG: 换passid 响应 Body: %s...\", res.String()[:min(len(res.String()), 500)])\n\n\tdycpwd := jsoniter.Get(res.Body(), \"var\", \"artifact\").ToString()\n\tif dycpwd == \"\" {\n\t\treturn \"\", errors.New(\"failed to extract dycpwd from artifact exchange response\")\n\t}\n\tlog.Debugf(\"DEBUG: 提取到 dycpwd: %s\", dycpwd)\n\n\treturn dycpwd, nil\n}\n\n// --- 辅助函数：加密/解密 ---\n\n// sha1Hash 计算 SHA1 哈希值，返回十六进制字符串。\nfunc sha1Hash(data string) string {\n\th := sha1.New()\n\th.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// pkcs7_pad PKCS7 填充\nfunc pkcs7_pad(data []byte, blockSize int) []byte {\n\tpadding := blockSize - len(data)%blockSize\n\tpadtext := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(data, padtext...)\n}\n\n// pkcs7_unpad PKCS7 去填充\nfunc pkcs7_unpad(data []byte) ([]byte, error) {\n\tlength := len(data)\n\tif length == 0 {\n\t\treturn nil, errors.New(\"pkcs7: data is empty\")\n\t}\n\tunpadding := int(data[length-1])\n\tif unpadding > length {\n\t\treturn nil, errors.New(\"pkcs7: invalid padding\")\n\t}\n\treturn data[:(length - unpadding)], nil\n}\n\n// aes_ecb_decrypt AES/ECB/Pkcs7 解密，输入为十六进制字符串。\nfunc aes_ecb_decrypt(ciphertext []byte, key []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(ciphertext)%block.BlockSize() != 0 {\n\t\treturn nil, errors.New(\"AES ECB decrypt: ciphertext is not a multiple of the block size\")\n\t}\n\n\tdecrypted := make([]byte, len(ciphertext))\n\tblockSize := block.BlockSize()\n\n\tfor bs, be := 0, blockSize; bs < len(ciphertext); bs, be = bs+blockSize, be+blockSize {\n\t\tblock.Decrypt(decrypted[bs:be], ciphertext[bs:be])\n\t}\n\n\treturn pkcs7_unpad(decrypted)\n}\n\n// 以下提供 camelCase 的 AES CBC 加解密，供文件中其它位置调用（并支持传入 IV）。\nfunc aesCbcEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(iv) != block.BlockSize() {\n\t\treturn nil, fmt.Errorf(\"aesCbcEncrypt: iv length %d does not match block size %d\", len(iv), block.BlockSize())\n\t}\n\tpadded := pkcs7_pad(plaintext, block.BlockSize())\n\tciphertext := make([]byte, len(padded))\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tmode.CryptBlocks(ciphertext, padded)\n\treturn ciphertext, nil\n}\n\nfunc aesCbcDecrypt(ciphertext []byte, key []byte, iv []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(iv) != block.BlockSize() {\n\t\treturn nil, fmt.Errorf(\"aesCbcDecrypt: iv length %d does not match block size %d\", len(iv), block.BlockSize())\n\t}\n\tif len(ciphertext)%block.BlockSize() != 0 {\n\t\treturn nil, errors.New(\"aesCbcDecrypt: ciphertext is not a multiple of the block size\")\n\t}\n\tdecrypted := make([]byte, len(ciphertext))\n\tmode := cipher.NewCBCDecrypter(block, iv)\n\tmode.CryptBlocks(decrypted, ciphertext)\n\treturn pkcs7_unpad(decrypted)\n}\n\n// sortedJsonStringify 对 JSON 对象进行排序并字符串化。\nfunc sortedJsonStringify(obj interface{}) (string, error) {\n\tif obj == nil {\n\t\treturn \"null\", nil\n\t}\n\n\tswitch v := obj.(type) {\n\tcase string:\n\t\t// 尝试解析为 JSON，如果成功则递归处理\n\t\tvar parsed interface{}\n\t\tif err := jsoniter.Unmarshal([]byte(v), &parsed); err == nil {\n\t\t\treturn sortedJsonStringify(parsed)\n\t\t}\n\t\t// 如果不是 JSON 字符串，则直接返回 JSON 字符串化的结果\n\t\treturn jsoniter.MarshalToString(v)\n\tcase int, float64, bool:\n\t\treturn fmt.Sprintf(\"%v\", v), nil\n\tcase []interface{}:\n\t\tvar items []string\n\t\tfor _, item := range v {\n\t\t\ts, err := sortedJsonStringify(item)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\titems = append(items, s)\n\t\t}\n\t\treturn fmt.Sprintf(\"[%s]\", strings.Join(items, \",\")), nil\n\tcase map[string]interface{}:\n\t\tsortedKeys := make([]string, 0, len(v))\n\t\tfor key := range v {\n\t\t\tsortedKeys = append(sortedKeys, key)\n\t\t}\n\t\tsort.Strings(sortedKeys)\n\n\t\tvar pairs []string\n\t\tfor _, key := range sortedKeys {\n\t\t\tvalue := v[key]\n\t\t\ts, err := sortedJsonStringify(value)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\t// Use jsoniter.MarshalToString for the key to ensure it's quoted correctly\n\t\t\tkeyStr, err := jsoniter.MarshalToString(key)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tpairs = append(pairs, fmt.Sprintf(\"%s:%s\", keyStr, s))\n\t\t}\n\t\treturn fmt.Sprintf(\"{%s}\", strings.Join(pairs, \",\")), nil\n\tdefault:\n\t\t// Fallback for other types, e.g., numbers, booleans, or unhandled complex types\n\t\t// Use jsoniter's default marshalling for these\n\t\treturn jsoniter.MarshalToString(v)\n\t}\n}\n\n// yun139EncryptedRequest handles the common encrypted request/response flow.\nfunc (d *Yun139) yun139EncryptedRequest(url string, body interface{}, headers map[string]string, aesKeyHex string, resp interface{}) ([]byte, error) {\n\t// 1. Decode AES key\n\taesKey, err := hex.DecodeString(aesKeyHex)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: failed to decode AES key: %w\", err)\n\t}\n\n\t// 2. Marshal and sort the request body\n\tsortedJson, err := sortedJsonStringify(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: failed to marshal and sort body: %w\", err)\n\t}\n\tlog.Debugf(\"yun139EncryptedRequest: Request Body (plaintext): %s\", sortedJson)\n\n\t// 3. Encrypt the body using AES/CBC\n\tiv := make([]byte, 16) // 16 bytes for AES-128\n\tif _, err := crypto_rand.Read(iv); err != nil {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: failed to generate IV: %w\", err)\n\t}\n\tencryptedBody, err := aesCbcEncrypt([]byte(sortedJson), aesKey, iv)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: failed to encrypt body: %w\", err)\n\t}\n\tpayload := base64.StdEncoding.EncodeToString(append(iv, encryptedBody...))\n\n\t// 4. Make the request\n\tres, err := base.RestyClient.R().\n\t\tSetHeaders(headers).\n\t\tSetBody(payload).\n\t\tPost(url)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: http request failed: %w\", err)\n\t}\n\n\tif res.StatusCode() != 200 {\n\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: unexpected status code %d: %s\", res.StatusCode(), res.String())\n\t}\n\n\t// 5. Decrypt the response\n\trespBody := res.Body()\n\tvar decryptedBytes []byte\n\n\tif len(respBody) > 0 && respBody[0] == '{' {\n\t\tlog.Warnf(\"yun139EncryptedRequest: received a plain JSON response, not an encrypted string. Body: %s\", string(respBody))\n\t\tdecryptedBytes = respBody\n\t} else {\n\t\tdecodedResp, err := base64.StdEncoding.DecodeString(string(respBody))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: response base64 decode failed: %w. Body: '%s'\", err, string(respBody))\n\t\t}\n\n\t\tif len(decodedResp) < 16 {\n\t\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: decoded response is too short to be encrypted. Length: %d\", len(decodedResp))\n\t\t}\n\n\t\trespIv := decodedResp[:16]\n\t\trespCiphertext := decodedResp[16:]\n\n\t\tdecryptedBytes, err = aesCbcDecrypt(respCiphertext, aesKey, respIv)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: response aes decrypt failed: %w\", err)\n\t\t}\n\t}\n\n\tlog.Debugf(\"yun139EncryptedRequest: Response Body (decrypted): %s\", string(decryptedBytes))\n\n\t// 6. Unmarshal to the final response struct\n\tif resp != nil {\n\t\terr = utils.Json.Unmarshal(decryptedBytes, resp)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"yun139EncryptedRequest: failed to unmarshal decrypted response: %w\", err)\n\t\t}\n\t}\n\n\treturn decryptedBytes, nil\n}\n\nfunc (d *Yun139) step3_third_party_login(dycpwd string) (string, error) {\n\tlog.Debugf(\"\\n--- 执行步骤 3: 单点登录 API ---\")\n\tssoLoginURL := \"https://user-njs.yun.139.com/user/thirdlogin\"\n\n\t// 构建原始请求体\n\tssoRequestBodyRaw := base.Json{\n\t\t\"clientkey_decrypt\": \"l3TryM&Q+X7@dzwk)qP\",\n\t\t\"clienttype\":        \"886\",\n\t\t\"cpid\":              \"507\",\n\t\t\"dycpwd\":            dycpwd,\n\t\t\"extInfo\":           base.Json{\"ifOpenAccount\": \"0\"},\n\t\t\"loginMode\":         \"0\",\n\t\t\"msisdn\":            d.Username,\n\t\t\"pintype\":           \"13\",\n\t\t\"secinfo\":           strings.ToUpper(sha1Hash(fmt.Sprintf(\"fetion.com.cn:%s\", dycpwd))),\n\t\t\"version\":           \"20250901\",\n\t}\n\n\tssoLoginHeaders := map[string]string{\n\t\t\"hcy-cool-flag\":       \"1\",\n\t\t\"x-huawei-channelSrc\": \"10246600\",\n\t\t\"x-sdk-channelSrc\":    \"\",\n\t\t\"x-MM-Source\":         \"0\",\n\t\t\"x-UserAgent\":         \"android|23116PN5BC|android15|1.2.6|||1440x3200|10246600\",\n\t\t\"x-DeviceInfo\":        \"4|127.0.0.1|5|1.2.6|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|||\",\n\t\t\"Content-Type\":        \"text/plain;charset=UTF-8\",\n\t\t\"Host\":                \"user-njs.yun.139.com\",\n\t\t\"Connection\":          \"Keep-Alive\",\n\t\t\"Accept-Encoding\":     \"gzip\",\n\t\t\"User-Agent\":          \"okhttp/3.12.2\",\n\t}\n\n\t// 使用通用加密请求函数\n\tdecryptedLayer1StrBytes, err := d.yun139EncryptedRequest(ssoLoginURL, ssoRequestBodyRaw, ssoLoginHeaders, KEY_HEX_1, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"step3 encrypted request failed: %w\", err)\n\t}\n\n\thexInner := jsoniter.Get(decryptedLayer1StrBytes, \"data\").ToString()\n\tif hexInner == \"\" {\n\t\treturn \"\", errors.New(\"missing data field in first layer decryption result\")\n\t}\n\tlog.Debugf(\"DEBUG: 第一层解密提取到 hex_inner: %s...\", hexInner[:min(len(hexInner), 50)])\n\n\t// 第二层解密\n\tkey2, err := hex.DecodeString(KEY_HEX_2)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode KEY_HEX_2: %w\", err)\n\t}\n\thexInnerBytes, err := hex.DecodeString(hexInner)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode hex_inner: %w\", err)\n\t}\n\tfinalJsonStrBytes, err := aes_ecb_decrypt(hexInnerBytes, key2)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"step3 response layer2 aes ecb decrypt failed: %w\", err)\n\t}\n\tlog.Debugf(\"DEBUG: 最终解密结果: %s\", string(finalJsonStrBytes))\n\n\t// 提取 authToken\n\tauthToken := jsoniter.Get(finalJsonStrBytes, \"authToken\").ToString()\n\tif authToken == \"\" {\n\t\treturn \"\", errors.New(\"failed to extract authToken from final decryption result\")\n\t}\n\tlog.Debugf(\"DEBUG: 提取到 authToken: %s\", authToken)\n\n\t// 提取 account 和 userDomainId\n\taccount := jsoniter.Get(finalJsonStrBytes, \"account\").ToString()\n\tuserDomainId := jsoniter.Get(finalJsonStrBytes, \"userDomainId\").ToString()\n\n\tif account == \"\" || userDomainId == \"\" {\n\t\treturn \"\", errors.New(\"failed to extract account or userDomainId from final decryption result\")\n\t}\n\n\td.UserDomainID = userDomainId\n\tnewAuthorization := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(\"pc:%s:%s\", account, authToken)))\n\treturn newAuthorization, nil\n}\n\nfunc (d *Yun139) loginWithPassword() (string, error) {\n\tif d.Username == \"\" || d.Password == \"\" || d.MailCookies == \"\" {\n\t\treturn \"\", errors.New(\"username, password or mail_cookies is empty\")\n\t}\n\n\tpassId, err := d.step1_password_login()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlog.Infof(\"Step 1 success, passId: %s\", passId)\n\n\ttoken, err := d.step2_get_single_token(passId)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlog.Infof(\"Step 2 success, token: %s\", token)\n\n\tnewAuth, err := d.step3_third_party_login(token)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlog.Infof(\"Step 3 success, new authorization generated.\")\n\n\td.Authorization = newAuth // Ensure Authorization is also updated before saving\n\top.MustSaveDriverStorage(d)\n\treturn newAuth, nil\n}\n\nfunc (d *Yun139) andAlbumRequest(pathname string, body interface{}, resp interface{}) ([]byte, error) {\n\turl := \"https://group.yun.139.com/hcy/family/adapter/andAlbum/openApi\" + pathname\n\n\theaders := map[string]string{\n\t\t\"Host\":                \"group.yun.139.com\",\n\t\t\"authorization\":       \"Basic \" + d.getAuthorization(),\n\t\t\"x-svctype\":           \"2\",\n\t\t\"hcy-cool-flag\":       \"1\",\n\t\t\"api-version\":         \"v2\",\n\t\t\"x-huawei-channelsrc\": \"10246600\",\n\t\t\"x-sdk-channelsrc\":    \"\",\n\t\t\"x-mm-source\":         \"0\",\n\t\t\"x-deviceinfo\":        \"1|127.0.0.1|1|12.3.2|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|zh||||032|0|\", //重要参数\n\t\t\"content-type\":        \"application/json; charset=utf-8\",\n\t\t\"user-agent\":          \"okhttp/4.11.0\",\n\t\t\"accept-encoding\":     \"gzip\",\n\t}\n\n\treturn d.yun139EncryptedRequest(url, body, headers, KEY_HEX_1, resp)\n}\n\nfunc (d *Yun139) handleMetaGroupCopy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tpathname := \"/copyContentCatalog\"\n\tvar sourceContentIDs []string\n\tvar sourceCatalogIDs []string\n\tif srcObj.IsDir() {\n\t\tsourceCatalogIDs = append(sourceCatalogIDs, path.Join(\"root:/\", srcObj.GetPath(), srcObj.GetID()))\n\t} else {\n\t\tsourceContentIDs = append(sourceContentIDs, path.Join(\"root:/\", srcObj.GetPath(), srcObj.GetID()))\n\t}\n\n\tdestCatalogID := path.Join(\"root:/\", dstDir.GetPath(), dstDir.GetID())\n\tlog.Debugf(\"[139Yun Group Copy] srcObj ID: %s, srcObj Path: %s, dstDir ID: %s, dstDir Path: %s, destCatalogID: %s\", srcObj.GetID(), srcObj.GetPath(), dstDir.GetID(), dstDir.GetPath(), destCatalogID)\n\n\tbody := base.Json{\n\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\"accountType\":   \"1\",\n\t\t\t\"accountUserId\": d.UserDomainID,\n\t\t},\n\t\t\"destCatalogID\":    destCatalogID,\n\t\t\"destCloudID\":      d.CloudID,\n\t\t\"sourceCatalogIDs\": sourceCatalogIDs,\n\t\t\"sourceCloudID\":    d.CloudID,\n\t\t\"sourceContentIDs\": sourceContentIDs,\n\t}\n\n\tvar resp base.Json\n\t_, err := d.andAlbumRequest(pathname, body, &resp)\n\treturn err\n}\n\n// getGroupRootByCloudID 查询 group 上层信息，优先返回 parentCatalogID，回退到 catalogList[0].path\nfunc (d *Yun139) getGroupRootByCloudID(cloudID string) (string, error) {\n\tpathname := \"/orchestration/group-rebuild/catalog/v1.0/queryGroupContentList\"\n\tbody := base.Json{\n\t\t\"groupID\": cloudID,\n\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\"account\":     d.getAccount(),\n\t\t\t\"accountType\": 1,\n\t\t},\n\t\t\"pageInfo\": base.Json{\n\t\t\t\"pageNum\":  1,\n\t\t\t\"pageSize\": 1,\n\t\t},\n\t}\n\tvar resp base.Json\n\t_, err := d.post(pathname, body, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdataObj, _ := resp[\"data\"].(map[string]interface{})\n\tif dataObj == nil {\n\t\treturn \"\", fmt.Errorf(\"invalid group response data\")\n\t}\n\tif gcr, ok := dataObj[\"getGroupContentResult\"].(map[string]interface{}); ok {\n\t\tif pid, ok := gcr[\"parentCatalogID\"].(string); ok && pid != \"\" {\n\t\t\treturn pid, nil\n\t\t}\n\t\tif cl, ok := gcr[\"catalogList\"].([]interface{}); ok && len(cl) > 0 {\n\t\t\tif first, ok := cl[0].(map[string]interface{}); ok {\n\t\t\t\tif p, ok := first[\"path\"].(string); ok && p != \"\" {\n\t\t\t\t\treturn p, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"no root found in group response\")\n}\n\n// getFamilyRootPath 查询 family 的上层 path（data.path）\n// 返回值已去除前缀 \"root:/\"（或 \"root:\"），直接返回纯 ID 或 path 部分，便于持久化为 RootFolderID。\nfunc (d *Yun139) getFamilyRootPath(cloudID string) (string, error) {\n\t// 使用 v1.2 接口（代码日志中已有该请求），pageSize 取 1 足够获取 path 字段\n\tpathname := \"/orchestration/familyCloud-rebuild/content/v1.2/queryContentList\"\n\tbody := base.Json{\n\t\t\"catalogID\":   \"\",\n\t\t\"catalogType\": 3,\n\t\t\"cloudID\":     cloudID,\n\t\t\"cloudType\":   1,\n\t\t\"commonAccountInfo\": base.Json{\n\t\t\t\"account\":     d.getAccount(),\n\t\t\t\"accountType\": 1,\n\t\t},\n\t\t\"contentSortType\": 0,\n\t\t\"pageInfo\": base.Json{\n\t\t\t\"pageNum\":  1,\n\t\t\t\"pageSize\": 1,\n\t\t},\n\t\t\"sortDirection\": 1,\n\t}\n\tvar resp base.Json\n\t_, err := d.post(pathname, body, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdataObj, _ := resp[\"data\"].(map[string]interface{})\n\tif dataObj == nil {\n\t\treturn \"\", fmt.Errorf(\"invalid family response data\")\n\t}\n\t// helper to strip \"root:/\" or \"root:\" prefix\n\tstripRoot := func(s string) string {\n\t\ts = strings.TrimSpace(s)\n\t\ts = strings.TrimPrefix(s, \"root:/\")\n\t\ts = strings.TrimPrefix(s, \"root:\")\n\t\treturn s\n\t}\n\tif p, ok := dataObj[\"path\"].(string); ok && p != \"\" {\n\t\treturn stripRoot(p), nil\n\t}\n\t// 回退：有时 path 在 cloudCatalogList.catalogList 中\n\tif cl, ok := dataObj[\"cloudCatalogList\"].([]interface{}); ok && len(cl) > 0 {\n\t\tif first, ok := cl[0].(map[string]interface{}); ok {\n\t\t\tif p, ok := first[\"path\"].(string); ok && p != \"\" {\n\t\t\t\treturn stripRoot(p), nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"no path found in family response\")\n}\n"
  },
  {
    "path": "drivers/189/driver.go",
    "content": "package _189\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Cloud189 struct {\n\tmodel.Storage\n\tAddition\n\tclient     *resty.Client\n\trsa        Rsa\n\tsessionKey string\n}\n\nfunc (d *Cloud189) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Cloud189) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Cloud189) Init(ctx context.Context) error {\n\td.client = base.NewRestyClient().\n\t\tSetHeader(\"Referer\", \"https://cloud.189.cn/\")\n\treturn d.newLogin()\n}\n\nfunc (d *Cloud189) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.getFiles(dir.GetID())\n}\n\nfunc (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp DownResp\n\tu := \"https://cloud.189.cn/api/portal/getFileInfo.action\"\n\t_, err := d.request(u, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"fileId\", file.GetID())\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := resty.NewWithClient(d.client.GetClient()).SetRedirectPolicy(\n\t\tresty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t}))\n\tres, err := client.R().SetHeader(\"User-Agent\", base.UserAgent).Get(\"https:\" + resp.FileDownloadUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugln(res.Status())\n\tlog.Debugln(res.String())\n\tlink := model.Link{}\n\tlog.Debugln(\"first url:\", resp.FileDownloadUrl)\n\tif res.StatusCode() == 302 {\n\t\tlink.URL = res.Header().Get(\"location\")\n\t\tlog.Debugln(\"second url:\", link.URL)\n\t\t_, _ = client.R().Get(link.URL)\n\t\tif res.StatusCode() == 302 {\n\t\t\tlink.URL = res.Header().Get(\"location\")\n\t\t}\n\t\tlog.Debugln(\"third url:\", link.URL)\n\t} else {\n\t\tlink.URL = resp.FileDownloadUrl\n\t}\n\tlink.URL = strings.Replace(link.URL, \"http://\", \"https://\", 1)\n\treturn &link, nil\n}\n\nfunc (d *Cloud189) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tform := map[string]string{\n\t\t\"parentFolderId\": parentDir.GetID(),\n\t\t\"folderName\":     dirName,\n\t}\n\t_, err := d.request(\"https://cloud.189.cn/api/open/file/createFolder.action\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(form)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tisFolder := 0\n\tif srcObj.IsDir() {\n\t\tisFolder = 1\n\t}\n\ttaskInfos := []base.Json{\n\t\t{\n\t\t\t\"fileId\":   srcObj.GetID(),\n\t\t\t\"fileName\": srcObj.GetName(),\n\t\t\t\"isFolder\": isFolder,\n\t\t},\n\t}\n\ttaskInfosBytes, err := utils.Json.Marshal(taskInfos)\n\tif err != nil {\n\t\treturn err\n\t}\n\tform := map[string]string{\n\t\t\"type\":           \"MOVE\",\n\t\t\"targetFolderId\": dstDir.GetID(),\n\t\t\"taskInfos\":      string(taskInfosBytes),\n\t}\n\t_, err = d.request(\"https://cloud.189.cn/api/open/batch/createBatchTask.action\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(form)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\turl := \"https://cloud.189.cn/api/open/file/renameFile.action\"\n\tidKey := \"fileId\"\n\tnameKey := \"destFileName\"\n\tif srcObj.IsDir() {\n\t\turl = \"https://cloud.189.cn/api/open/file/renameFolder.action\"\n\t\tidKey = \"folderId\"\n\t\tnameKey = \"destFolderName\"\n\t}\n\tform := map[string]string{\n\t\tidKey:   srcObj.GetID(),\n\t\tnameKey: newName,\n\t}\n\t_, err := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(form)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tisFolder := 0\n\tif srcObj.IsDir() {\n\t\tisFolder = 1\n\t}\n\ttaskInfos := []base.Json{\n\t\t{\n\t\t\t\"fileId\":   srcObj.GetID(),\n\t\t\t\"fileName\": srcObj.GetName(),\n\t\t\t\"isFolder\": isFolder,\n\t\t},\n\t}\n\ttaskInfosBytes, err := utils.Json.Marshal(taskInfos)\n\tif err != nil {\n\t\treturn err\n\t}\n\tform := map[string]string{\n\t\t\"type\":           \"COPY\",\n\t\t\"targetFolderId\": dstDir.GetID(),\n\t\t\"taskInfos\":      string(taskInfosBytes),\n\t}\n\t_, err = d.request(\"https://cloud.189.cn/api/open/batch/createBatchTask.action\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(form)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) Remove(ctx context.Context, obj model.Obj) error {\n\tisFolder := 0\n\tif obj.IsDir() {\n\t\tisFolder = 1\n\t}\n\ttaskInfos := []base.Json{\n\t\t{\n\t\t\t\"fileId\":   obj.GetID(),\n\t\t\t\"fileName\": obj.GetName(),\n\t\t\t\"isFolder\": isFolder,\n\t\t},\n\t}\n\ttaskInfosBytes, err := utils.Json.Marshal(taskInfos)\n\tif err != nil {\n\t\treturn err\n\t}\n\tform := map[string]string{\n\t\t\"type\":           \"DELETE\",\n\t\t\"targetFolderId\": \"\",\n\t\t\"taskInfos\":      string(taskInfosBytes),\n\t}\n\t_, err = d.request(\"https://cloud.189.cn/api/open/batch/createBatchTask.action\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(form)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\treturn d.newUpload(ctx, dstDir, stream, up)\n}\n\nfunc (d *Cloud189) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tcapacityInfo, err := d.getCapacityInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: capacityInfo.CloudCapacityInfo.TotalSize,\n\t\t\tUsedSpace:  capacityInfo.CloudCapacityInfo.UsedSize,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Cloud189)(nil)\n"
  },
  {
    "path": "drivers/189/help.go",
    "content": "package _189\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha1\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\tmyrand \"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc random() string {\n\treturn fmt.Sprintf(\"0.%17v\", myrand.Rand.Int63n(100000000000000000))\n}\n\nfunc RsaEncode(origData []byte, j_rsakey string, hex bool) string {\n\tpublicKey := []byte(\"-----BEGIN PUBLIC KEY-----\\n\" + j_rsakey + \"\\n-----END PUBLIC KEY-----\")\n\tblock, _ := pem.Decode(publicKey)\n\tpubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)\n\tpub := pubInterface.(*rsa.PublicKey)\n\tb, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)\n\tif err != nil {\n\t\tlog.Errorf(\"err: %s\", err.Error())\n\t}\n\tres := base64.StdEncoding.EncodeToString(b)\n\tif hex {\n\t\treturn b64tohex(res)\n\t}\n\treturn res\n}\n\nvar b64map = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n\nvar BI_RM = \"0123456789abcdefghijklmnopqrstuvwxyz\"\n\nfunc int2char(a int) string {\n\treturn strings.Split(BI_RM, \"\")[a]\n}\n\nfunc b64tohex(a string) string {\n\td := \"\"\n\te := 0\n\tc := 0\n\tfor i := 0; i < len(a); i++ {\n\t\tm := strings.Split(a, \"\")[i]\n\t\tif m != \"=\" {\n\t\t\tv := strings.Index(b64map, m)\n\t\t\tif 0 == e {\n\t\t\t\te = 1\n\t\t\t\td += int2char(v >> 2)\n\t\t\t\tc = 3 & v\n\t\t\t} else if 1 == e {\n\t\t\t\te = 2\n\t\t\t\td += int2char(c<<2 | v>>4)\n\t\t\t\tc = 15 & v\n\t\t\t} else if 2 == e {\n\t\t\t\te = 3\n\t\t\t\td += int2char(c)\n\t\t\t\td += int2char(v >> 2)\n\t\t\t\tc = 3 & v\n\t\t\t} else {\n\t\t\t\te = 0\n\t\t\t\td += int2char(c<<2 | v>>4)\n\t\t\t\td += int2char(15 & v)\n\t\t\t}\n\t\t}\n\t}\n\tif e == 1 {\n\t\td += int2char(c << 2)\n\t}\n\treturn d\n}\n\nfunc qs(form map[string]string) string {\n\tf := make(url.Values)\n\tfor k, v := range form {\n\t\tf.Set(k, v)\n\t}\n\treturn EncodeParam(f)\n\t//strList := make([]string, 0)\n\t//for k, v := range form {\n\t//\tstrList = append(strList, fmt.Sprintf(\"%s=%s\", k, url.QueryEscape(v)))\n\t//}\n\t//return strings.Join(strList, \"&\")\n}\n\nfunc EncodeParam(v url.Values) string {\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\tvar buf strings.Builder\n\tkeys := make([]string, 0, len(v))\n\tfor k := range v {\n\t\tkeys = append(keys, k)\n\t}\n\tfor _, k := range keys {\n\t\tvs := v[k]\n\t\tfor _, v := range vs {\n\t\t\tif buf.Len() > 0 {\n\t\t\t\tbuf.WriteByte('&')\n\t\t\t}\n\t\t\tbuf.WriteString(k)\n\t\t\tbuf.WriteByte('=')\n\t\t\t//if k == \"fileName\" {\n\t\t\t//\tbuf.WriteString(encode(v))\n\t\t\t//} else {\n\t\t\tbuf.WriteString(v)\n\t\t\t//}\n\t\t}\n\t}\n\treturn buf.String()\n}\n\nfunc encode(str string) string {\n\t//str = strings.ReplaceAll(str, \"%\", \"%25\")\n\t//str = strings.ReplaceAll(str, \"&\", \"%26\")\n\t//str = strings.ReplaceAll(str, \"+\", \"%2B\")\n\t//return str\n\treturn url.QueryEscape(str)\n}\n\nfunc AesEncrypt(data, key []byte) []byte {\n\tblock, _ := aes.NewCipher(key)\n\tif block == nil {\n\t\treturn []byte{}\n\t}\n\tdata = PKCS7Padding(data, block.BlockSize())\n\tdecrypted := make([]byte, len(data))\n\tsize := block.BlockSize()\n\tfor bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {\n\t\tblock.Encrypt(decrypted[bs:be], data[bs:be])\n\t}\n\treturn decrypted\n}\n\nfunc PKCS7Padding(ciphertext []byte, blockSize int) []byte {\n\tpadding := blockSize - len(ciphertext)%blockSize\n\tpadtext := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(ciphertext, padtext...)\n}\n\nfunc hmacSha1(data string, secret string) string {\n\th := hmac.New(sha1.New, []byte(secret))\n\th.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\nfunc getMd5(data []byte) []byte {\n\th := md5.New()\n\th.Write(data)\n\treturn h.Sum(nil)\n}\n\nfunc decodeURIComponent(str string) string {\n\tr, _ := url.PathUnescape(str)\n\t//r = strings.ReplaceAll(r, \" \", \"+\")\n\treturn r\n}\n\nfunc Random(v string) string {\n\treg := regexp.MustCompilePOSIX(\"[xy]\")\n\tdata := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte {\n\t\tvar i int64\n\t\tt := int64(16 * myrand.Rand.Float32())\n\t\tif msg[0] == 120 {\n\t\t\ti = t\n\t\t} else {\n\t\t\ti = 3&t | 8\n\t\t}\n\t\treturn []byte(strconv.FormatInt(i, 16))\n\t})\n\treturn string(data)\n}\n"
  },
  {
    "path": "drivers/189/login.go",
    "content": "package _189\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AppConf struct {\n\tData struct {\n\t\tAccountType     string `json:\"accountType\"`\n\t\tAgreementCheck  string `json:\"agreementCheck\"`\n\t\tAppKey          string `json:\"appKey\"`\n\t\tClientType      int    `json:\"clientType\"`\n\t\tIsOauth2        bool   `json:\"isOauth2\"`\n\t\tLoginSort       string `json:\"loginSort\"`\n\t\tMailSuffix      string `json:\"mailSuffix\"`\n\t\tPageKey         string `json:\"pageKey\"`\n\t\tParamId         string `json:\"paramId\"`\n\t\tRegReturnUrl    string `json:\"regReturnUrl\"`\n\t\tReqId           string `json:\"reqId\"`\n\t\tReturnUrl       string `json:\"returnUrl\"`\n\t\tShowFeedback    string `json:\"showFeedback\"`\n\t\tShowPwSaveName  string `json:\"showPwSaveName\"`\n\t\tShowQrSaveName  string `json:\"showQrSaveName\"`\n\t\tShowSmsSaveName string `json:\"showSmsSaveName\"`\n\t\tSso             string `json:\"sso\"`\n\t} `json:\"data\"`\n\tMsg    string `json:\"msg\"`\n\tResult string `json:\"result\"`\n}\n\ntype EncryptConf struct {\n\tResult int `json:\"result\"`\n\tData   struct {\n\t\tUpSmsOn   string `json:\"upSmsOn\"`\n\t\tPre       string `json:\"pre\"`\n\t\tPreDomain string `json:\"preDomain\"`\n\t\tPubKey    string `json:\"pubKey\"`\n\t} `json:\"data\"`\n}\n\nfunc (d *Cloud189) newLogin() error {\n\turl := \"https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action\"\n\tres, err := d.client.R().Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Is logged in\n\tredirectURL := res.RawResponse.Request.URL\n\tif redirectURL.String() == \"https://cloud.189.cn/web/main\" {\n\t\treturn nil\n\t}\n\tlt := redirectURL.Query().Get(\"lt\")\n\treqId := redirectURL.Query().Get(\"reqId\")\n\tappId := redirectURL.Query().Get(\"appId\")\n\theaders := map[string]string{\n\t\t\"lt\":      lt,\n\t\t\"reqid\":   reqId,\n\t\t\"referer\": redirectURL.String(),\n\t\t\"origin\":  \"https://open.e.189.cn\",\n\t}\n\t// get app Conf\n\tvar appConf AppConf\n\tres, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{\n\t\t\"version\": \"2.0\",\n\t\t\"appKey\":  appId,\n\t}).SetResult(&appConf).Post(\"https://open.e.189.cn/api/logbox/oauth2/appConf.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"189 AppConf resp body: %s\", res.String())\n\tif appConf.Result != \"0\" {\n\t\treturn errors.New(appConf.Msg)\n\t}\n\t// get encrypt conf\n\tvar encryptConf EncryptConf\n\tres, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{\n\t\t\"appId\": appId,\n\t}).Post(\"https://open.e.189.cn/api/logbox/config/encryptConf.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = utils.Json.Unmarshal(res.Body(), &encryptConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"189 EncryptConf resp body: %s\\n%+v\", res.String(), encryptConf)\n\tif encryptConf.Result != 0 {\n\t\treturn errors.New(\"get EncryptConf error:\" + res.String())\n\t}\n\t// TODO: getUUID? needcaptcha\n\t// login\n\tloginData := map[string]string{\n\t\t\"version\":         \"v2.0\",\n\t\t\"apToken\":         \"\",\n\t\t\"appKey\":          appId,\n\t\t\"accountType\":     appConf.Data.AccountType,\n\t\t\"userName\":        encryptConf.Data.Pre + RsaEncode([]byte(d.Username), encryptConf.Data.PubKey, true),\n\t\t\"epd\":             encryptConf.Data.Pre + RsaEncode([]byte(d.Password), encryptConf.Data.PubKey, true),\n\t\t\"captchaType\":     \"\",\n\t\t\"validateCode\":    \"\",\n\t\t\"smsValidateCode\": \"\",\n\t\t\"captchaToken\":    \"\",\n\t\t\"returnUrl\":       appConf.Data.ReturnUrl,\n\t\t\"mailSuffix\":      appConf.Data.MailSuffix,\n\t\t\"dynamicCheck\":    \"FALSE\",\n\t\t\"clientType\":      strconv.Itoa(appConf.Data.ClientType),\n\t\t\"cb_SaveName\":     \"3\",\n\t\t\"isOauth2\":        strconv.FormatBool(appConf.Data.IsOauth2),\n\t\t\"state\":           \"\",\n\t\t\"paramId\":         appConf.Data.ParamId,\n\t}\n\tres, err = d.client.R().SetHeaders(headers).SetFormData(loginData).Post(\"https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"189 login resp body: %s\", res.String())\n\tloginResult := utils.Json.Get(res.Body(), \"result\").ToInt()\n\tif loginResult != 0 {\n\t\treturn errors.New(utils.Json.Get(res.Body(), \"msg\").ToString())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/189/meta.go",
    "content": "package _189\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tUsername string `json:\"username\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\tCookie   string `json:\"cookie\" help:\"Fill in the cookie if need captcha\"`\n\tdriver.RootID\n}\n\nvar config = driver.Config{\n\tName:        \"189Cloud\",\n\tLocalSort:   true,\n\tDefaultRoot: \"-11\",\n\tAlert:       `info|You can try to use 189PC driver if this driver does not work.`,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Cloud189{}\n\t})\n}\n"
  },
  {
    "path": "drivers/189/types.go",
    "content": "package _189\n\ntype LoginResp struct {\n\tMsg    string `json:\"msg\"`\n\tResult int    `json:\"result\"`\n\tToUrl  string `json:\"toUrl\"`\n}\n\ntype Error struct {\n\tErrorCode string `json:\"errorCode\"`\n\tErrorMsg  string `json:\"errorMsg\"`\n}\n\ntype File struct {\n\tId         int64  `json:\"id\"`\n\tLastOpTime string `json:\"lastOpTime\"`\n\tName       string `json:\"name\"`\n\tSize       int64  `json:\"size\"`\n\tIcon       struct {\n\t\tSmallUrl string `json:\"smallUrl\"`\n\t\t//LargeUrl string `json:\"largeUrl\"`\n\t} `json:\"icon\"`\n\tUrl string `json:\"url\"`\n}\n\ntype Folder struct {\n\tId         int64  `json:\"id\"`\n\tLastOpTime string `json:\"lastOpTime\"`\n\tName       string `json:\"name\"`\n}\n\ntype Files struct {\n\tResCode    int    `json:\"res_code\"`\n\tResMessage string `json:\"res_message\"`\n\tFileListAO struct {\n\t\tCount      int      `json:\"count\"`\n\t\tFileList   []File   `json:\"fileList\"`\n\t\tFolderList []Folder `json:\"folderList\"`\n\t} `json:\"fileListAO\"`\n}\n\ntype UploadUrlsResp struct {\n\tCode       string          `json:\"code\"`\n\tUploadUrls map[string]Part `json:\"uploadUrls\"`\n}\n\ntype Part struct {\n\tRequestURL    string `json:\"requestURL\"`\n\tRequestHeader string `json:\"requestHeader\"`\n}\n\ntype Rsa struct {\n\tExpire int64  `json:\"expire\"`\n\tPkId   string `json:\"pkId\"`\n\tPubKey string `json:\"pubKey\"`\n}\n\ntype Down struct {\n\tResCode         int    `json:\"res_code\"`\n\tResMessage      string `json:\"res_message\"`\n\tFileDownloadUrl string `json:\"fileDownloadUrl\"`\n}\n\ntype DownResp struct {\n\tResCode         int    `json:\"res_code\"`\n\tResMessage      string `json:\"res_message\"`\n\tFileDownloadUrl string `json:\"downloadUrl\"`\n}\n\ntype CapacityResp struct {\n\tResCode           int    `json:\"res_code\"`\n\tResMessage        string `json:\"res_message\"`\n\tAccount           string `json:\"account\"`\n\tCloudCapacityInfo struct {\n\t\tFreeSize     int64 `json:\"freeSize\"`\n\t\tMailUsedSize int64 `json:\"mail189UsedSize\"`\n\t\tTotalSize    int64 `json:\"totalSize\"`\n\t\tUsedSize     int64 `json:\"usedSize\"`\n\t} `json:\"cloudCapacityInfo\"`\n\tFamilyCapacityInfo struct {\n\t\tFreeSize  int64 `json:\"freeSize\"`\n\t\tTotalSize int64 `json:\"totalSize\"`\n\t\tUsedSize  int64 `json:\"usedSize\"`\n\t} `json:\"familyCapacityInfo\"`\n\tTotalSize uint64 `json:\"totalSize\"`\n}\n"
  },
  {
    "path": "drivers/189/util.go",
    "content": "package _189\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tmyrand \"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\n//func (d *Cloud189) login() error {\n//\turl := \"https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action\"\n//\tb := \"\"\n//\tlt := \"\"\n//\tltText := regexp.MustCompile(`lt = \"(.+?)\"`)\n//\tvar res *resty.Response\n//\tvar err error\n//\tfor i := 0; i < 3; i++ {\n//\t\tres, err = d.client.R().Get(url)\n//\t\tif err != nil {\n//\t\t\treturn err\n//\t\t}\n//\t\t// 已经登陆\n//\t\tif res.RawResponse.Request.URL.String() == \"https://cloud.189.cn/web/main\" {\n//\t\t\treturn nil\n//\t\t}\n//\t\tb = res.String()\n//\t\tltTextArr := ltText.FindStringSubmatch(b)\n//\t\tif len(ltTextArr) > 0 {\n//\t\t\tlt = ltTextArr[1]\n//\t\t\tbreak\n//\t\t} else {\n//\t\t\t<-time.After(time.Second)\n//\t\t}\n//\t}\n//\tif lt == \"\" {\n//\t\treturn fmt.Errorf(\"get page: %s \\nstatus: %d \\nrequest url: %s\\nredirect url: %s\",\n//\t\t\tb, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get(\"location\"))\n//\t}\n//\tcaptchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]\n//\treturnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]\n//\tparamId := regexp.MustCompile(`paramId = \"(.+?)\"`).FindStringSubmatch(b)[1]\n//\t//reqId := regexp.MustCompile(`reqId = \"(.+?)\"`).FindStringSubmatch(b)[1]\n//\tjRsakey := regexp.MustCompile(`j_rsaKey\" value=\"(\\S+)\"`).FindStringSubmatch(b)[1]\n//\tvCodeID := regexp.MustCompile(`picCaptcha\\.do\\?token\\=([A-Za-z0-9\\&\\=]+)`).FindStringSubmatch(b)[1]\n//\tvCodeRS := \"\"\n//\tif vCodeID != \"\" {\n//\t\t// need ValidateCode\n//\t\tlog.Debugf(\"try to identify verification codes\")\n//\t\ttimeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)\n//\t\tu := \"https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=\" + vCodeID + timeStamp\n//\t\timgRes, err := d.client.R().SetHeaders(map[string]string{\n//\t\t\t\"User-Agent\":     \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0\",\n//\t\t\t\"Referer\":        \"https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do\",\n//\t\t\t\"Sec-Fetch-Dest\": \"image\",\n//\t\t\t\"Sec-Fetch-Mode\": \"no-cors\",\n//\t\t\t\"Sec-Fetch-Site\": \"same-origin\",\n//\t\t}).Get(u)\n//\t\tif err != nil {\n//\t\t\treturn err\n//\t\t}\n//\t\t// Enter the verification code manually\n//\t\t//err = message.GetMessenger().WaitSend(message.Message{\n//\t\t//\tType:    \"image\",\n//\t\t//\tContent: \"data:image/png;base64,\" + base64.StdEncoding.EncodeToString(imgRes.Body()),\n//\t\t//}, 10)\n//\t\t//if err != nil {\n//\t\t//\treturn err\n//\t\t//}\n//\t\t//vCodeRS, err = message.GetMessenger().WaitReceive(30)\n//\t\t// use ocr api\n//\t\tvRes, err := base.RestyClient.R().SetMultipartField(\n//\t\t\t\"image\", \"validateCode.png\", \"image/png\", bytes.NewReader(imgRes.Body())).\n//\t\t\tPost(setting.GetStr(conf.OcrApi))\n//\t\tif err != nil {\n//\t\t\treturn err\n//\t\t}\n//\t\tif jsoniter.Get(vRes.Body(), \"status\").ToInt() != 200 {\n//\t\t\treturn errors.New(\"ocr error:\" + jsoniter.Get(vRes.Body(), \"msg\").ToString())\n//\t\t}\n//\t\tvCodeRS = jsoniter.Get(vRes.Body(), \"result\").ToString()\n//\t\tlog.Debugln(\"code: \", vCodeRS)\n//\t}\n//\tuserRsa := RsaEncode([]byte(d.Username), jRsakey, true)\n//\tpasswordRsa := RsaEncode([]byte(d.Password), jRsakey, true)\n//\turl = \"https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do\"\n//\tvar loginResp LoginResp\n//\tres, err = d.client.R().\n//\t\tSetHeaders(map[string]string{\n//\t\t\t\"lt\":         lt,\n//\t\t\t\"User-Agent\": base.UserAgentNT,\n//\t\t\t\"Referer\":    \"https://open.e.189.cn/\",\n//\t\t\t\"accept\":     \"application/json;charset=UTF-8\",\n//\t\t}).SetFormData(map[string]string{\n//\t\t\"appKey\":       \"cloud\",\n//\t\t\"accountType\":  \"01\",\n//\t\t\"userName\":     \"{RSA}\" + userRsa,\n//\t\t\"password\":     \"{RSA}\" + passwordRsa,\n//\t\t\"validateCode\": vCodeRS,\n//\t\t\"captchaToken\": captchaToken,\n//\t\t\"returnUrl\":    returnUrl,\n//\t\t\"mailSuffix\":   \"@pan.cn\",\n//\t\t\"paramId\":      paramId,\n//\t\t\"clientType\":   \"10010\",\n//\t\t\"dynamicCheck\": \"FALSE\",\n//\t\t\"cb_SaveName\":  \"1\",\n//\t\t\"isOauth2\":     \"false\",\n//\t}).Post(url)\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\terr = utils.Json.Unmarshal(res.Body(), &loginResp)\n//\tif err != nil {\n//\t\tlog.Error(err.Error())\n//\t\treturn err\n//\t}\n//\tif loginResp.Result != 0 {\n//\t\treturn fmt.Errorf(loginResp.Msg)\n//\t}\n//\t_, err = d.client.R().Get(loginResp.ToUrl)\n//\treturn err\n//}\n\nfunc (d *Cloud189) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tvar e Error\n\treq := d.client.R().SetError(&e).\n\t\tSetHeader(\"Accept\", \"application/json;charset=UTF-8\").\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"noCache\": random(),\n\t\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// log.Debug(res.String())\n\tif e.ErrorCode != \"\" {\n\t\tif e.ErrorCode == \"InvalidSessionKey\" {\n\t\t\terr = d.newLogin()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(url, method, callback, resp)\n\t\t}\n\t}\n\tif jsoniter.Get(res.Body(), \"res_code\").ToInt() != 0 {\n\t\terr = errors.New(jsoniter.Get(res.Body(), \"res_message\").ToString())\n\t}\n\treturn res.Body(), err\n}\n\nfunc (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {\n\tres := make([]model.Obj, 0)\n\tpageNum := 1\n\tfor {\n\t\tvar resp Files\n\t\t_, err := d.request(\"https://cloud.189.cn/api/open/file/listFiles.action\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t//\"noCache\":    random(),\n\t\t\t\t\"pageSize\":   \"60\",\n\t\t\t\t\"pageNum\":    strconv.Itoa(pageNum),\n\t\t\t\t\"mediaType\":  \"0\",\n\t\t\t\t\"folderId\":   fileId,\n\t\t\t\t\"iconOption\": \"5\",\n\t\t\t\t\"orderBy\":    \"lastOpTime\", // account.OrderBy\n\t\t\t\t\"descending\": \"true\",       // account.OrderDirection\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resp.FileListAO.Count == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfor _, folder := range resp.FileListAO.FolderList {\n\t\t\tlastOpTime := utils.MustParseCNTime(folder.LastOpTime)\n\t\t\tres = append(res, &model.Object{\n\t\t\t\tID:       strconv.FormatInt(folder.Id, 10),\n\t\t\t\tName:     folder.Name,\n\t\t\t\tModified: lastOpTime,\n\t\t\t\tIsFolder: true,\n\t\t\t})\n\t\t}\n\t\tfor _, file := range resp.FileListAO.FileList {\n\t\t\tlastOpTime := utils.MustParseCNTime(file.LastOpTime)\n\t\t\tres = append(res, &model.ObjThumb{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       strconv.FormatInt(file.Id, 10),\n\t\t\t\t\tName:     file.Name,\n\t\t\t\t\tModified: lastOpTime,\n\t\t\t\t\tSize:     file.Size,\n\t\t\t\t},\n\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: file.Icon.SmallUrl},\n\t\t\t})\n\t\t}\n\t\tpageNum++\n\t}\n\treturn res, nil\n}\n\nfunc (d *Cloud189) oldUpload(dstDir model.Obj, file model.FileStreamer) error {\n\tres, err := d.client.R().SetMultipartFormData(map[string]string{\n\t\t\"parentId\":   dstDir.GetID(),\n\t\t\"sessionKey\": \"??\",\n\t\t\"opertype\":   \"1\",\n\t\t\"fname\":      file.GetName(),\n\t}).SetMultipartField(\"Filedata\", file.GetName(), file.GetMimetype(), file).Post(\"https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif utils.Json.Get(res.Body(), \"MD5\").ToString() != \"\" {\n\t\treturn nil\n\t}\n\tlog.Debugf(res.String())\n\treturn errors.New(res.String())\n}\n\nfunc (d *Cloud189) getSessionKey() (string, error) {\n\tresp, err := d.request(\"https://cloud.189.cn/v2/getUserBriefInfo.action\", http.MethodGet, nil, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tsessionKey := utils.Json.Get(resp, \"sessionKey\").ToString()\n\treturn sessionKey, nil\n}\n\nfunc (d *Cloud189) getResKey() (string, string, error) {\n\tnow := time.Now().UnixMilli()\n\tif d.rsa.Expire > now {\n\t\treturn d.rsa.PubKey, d.rsa.PkId, nil\n\t}\n\tresp, err := d.request(\"https://cloud.189.cn/api/security/generateRsaKey.action\", http.MethodGet, nil, nil)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tpubKey, pkId := utils.Json.Get(resp, \"pubKey\").ToString(), utils.Json.Get(resp, \"pkId\").ToString()\n\td.rsa.PubKey, d.rsa.PkId = pubKey, pkId\n\td.rsa.Expire = utils.Json.Get(resp, \"expire\").ToInt64()\n\treturn pubKey, pkId, nil\n}\n\nfunc (d *Cloud189) uploadRequest(uri string, form map[string]string, resp interface{}) ([]byte, error) {\n\tc := strconv.FormatInt(time.Now().UnixMilli(), 10)\n\tr := Random(\"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\")\n\tl := Random(\"xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx\")\n\tl = l[0 : 16+int(16*myrand.Rand.Float32())]\n\n\te := qs(form)\n\tdata := AesEncrypt([]byte(e), []byte(l[0:16]))\n\th := hex.EncodeToString(data)\n\n\tsessionKey := d.sessionKey\n\tsignature := hmacSha1(fmt.Sprintf(\"SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s&params=%s\", sessionKey, uri, c, h), l)\n\n\tpubKey, pkId, err := d.getResKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tb := RsaEncode([]byte(l), pubKey, false)\n\treq := d.client.R().SetHeaders(map[string]string{\n\t\t\"accept\":         \"application/json;charset=UTF-8\",\n\t\t\"SessionKey\":     sessionKey,\n\t\t\"Signature\":      signature,\n\t\t\"X-Request-Date\": c,\n\t\t\"X-Request-ID\":   r,\n\t\t\"EncryptionText\": b,\n\t\t\"PkId\":           pkId,\n\t})\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Get(\"https://upload.cloud.189.cn\" + uri + \"?params=\" + h)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata = res.Body()\n\tif utils.Json.Get(data, \"code\").ToString() != \"SUCCESS\" {\n\t\treturn nil, errors.New(uri + \"---\" + jsoniter.Get(data, \"msg\").ToString())\n\t}\n\treturn data, nil\n}\n\nfunc (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tsessionKey, err := d.getSessionKey()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.sessionKey = sessionKey\n\tconst DEFAULT int64 = 10485760\n\tcount := int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))\n\n\tres, err := d.uploadRequest(\"/person/initMultiUpload\", map[string]string{\n\t\t\"parentFolderId\": dstDir.GetID(),\n\t\t\"fileName\":       encode(file.GetName()),\n\t\t\"fileSize\":       strconv.FormatInt(file.GetSize(), 10),\n\t\t\"sliceSize\":      strconv.FormatInt(DEFAULT, 10),\n\t\t\"lazyCheck\":      \"1\",\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuploadFileId := jsoniter.Get(res, \"data\", \"uploadFileId\").ToString()\n\t//_, err = d.uploadRequest(\"/person/getUploadedPartsInfo\", map[string]string{\n\t//\t\"uploadFileId\": uploadFileId,\n\t//}, nil)\n\tvar finish int64 = 0\n\tvar i int64\n\tvar byteSize int64\n\tmd5s := make([]string, 0)\n\tmd5Sum := md5.New()\n\tfor i = 1; i <= count; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tbyteSize = file.GetSize() - finish\n\t\tif DEFAULT < byteSize {\n\t\t\tbyteSize = DEFAULT\n\t\t}\n\t\t// log.Debugf(\"%d,%d\", byteSize, finish)\n\t\tbyteData := make([]byte, byteSize)\n\t\tn, err := io.ReadFull(file, byteData)\n\t\t// log.Debug(err, n)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += int64(n)\n\t\tmd5Bytes := getMd5(byteData)\n\t\tmd5Hex := hex.EncodeToString(md5Bytes)\n\t\tmd5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)\n\t\tmd5s = append(md5s, strings.ToUpper(md5Hex))\n\t\tmd5Sum.Write(byteData)\n\t\tvar resp UploadUrlsResp\n\t\tres, err = d.uploadRequest(\"/person/getMultiUploadUrls\", map[string]string{\n\t\t\t\"partInfo\":     fmt.Sprintf(\"%s-%s\", strconv.FormatInt(i, 10), md5Base64),\n\t\t\t\"uploadFileId\": uploadFileId,\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuploadData := resp.UploadUrls[\"partNumber_\"+strconv.FormatInt(i, 10)]\n\t\tlog.Debugf(\"uploadData: %+v\", uploadData)\n\t\trequestURL := uploadData.RequestURL\n\t\tuploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), \"&\")\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, requestURL, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, v := range uploadHeaders {\n\t\t\ti := strings.Index(v, \"=\")\n\t\t\treq.Header.Set(v[0:i], v[i+1:])\n\t\t}\n\t\tr, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debugf(\"%+v %+v\", r, r.Request.Header)\n\t\t_ = r.Body.Close()\n\t\tup(float64(i) * 100 / float64(count))\n\t}\n\tfileMd5 := hex.EncodeToString(md5Sum.Sum(nil))\n\tsliceMd5 := fileMd5\n\tif file.GetSize() > DEFAULT {\n\t\tsliceMd5 = utils.GetMD5EncodeStr(strings.Join(md5s, \"\\n\"))\n\t}\n\tres, err = d.uploadRequest(\"/person/commitMultiUploadFile\", map[string]string{\n\t\t\"uploadFileId\": uploadFileId,\n\t\t\"fileMd5\":      fileMd5,\n\t\t\"sliceMd5\":     sliceMd5,\n\t\t\"lazyCheck\":    \"1\",\n\t\t\"opertype\":     \"3\",\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloud189) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {\n\tvar resp CapacityResp\n\t_, err := d.request(\"https://cloud.189.cn/api/portal/getUserSizeInfo.action\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/189_tv/driver.go",
    "content": "package _189_tv\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Cloud189TV struct {\n\tmodel.Storage\n\tAddition\n\tclient        *resty.Client\n\ttokenInfo     *AppSessionResp\n\tuploadThread  int\n\tstorageConfig driver.Config\n\n\tTempUuid string\n\tcron     *cron.Cron // 新增 cron 字段\n}\n\nfunc (y *Cloud189TV) Config() driver.Config {\n\tif y.storageConfig.Name == \"\" {\n\t\ty.storageConfig = config\n\t}\n\treturn y.storageConfig\n}\n\nfunc (y *Cloud189TV) GetAddition() driver.Additional {\n\treturn &y.Addition\n}\n\nfunc (y *Cloud189TV) Init(ctx context.Context) (err error) {\n\t// 兼容旧上传接口\n\ty.storageConfig.NoOverwriteUpload = y.isFamily() && y.Addition.RapidUpload\n\n\t// 处理个人云和家庭云参数\n\tif y.isFamily() && y.RootFolderID == \"-11\" {\n\t\ty.RootFolderID = \"\"\n\t}\n\tif !y.isFamily() && y.RootFolderID == \"\" {\n\t\ty.RootFolderID = \"-11\"\n\t}\n\n\t// 限制上传线程数\n\ty.uploadThread, _ = strconv.Atoi(y.UploadThread)\n\tif y.uploadThread < 1 || y.uploadThread > 32 {\n\t\ty.uploadThread, y.UploadThread = 3, \"3\"\n\t}\n\n\t// 初始化请求客户端\n\tif y.client == nil {\n\t\ty.client = base.NewRestyClient().SetHeaders(\n\t\t\tmap[string]string{\n\t\t\t\t\"Accept\":     \"application/json;charset=UTF-8\",\n\t\t\t\t\"User-Agent\": \"EcloudTV/6.5.5 (PJX110; unknown; home02) Android/35\",\n\t\t\t},\n\t\t)\n\t}\n\n\t// 避免重复登陆\n\tif !y.isLogin() || y.Addition.AccessToken == \"\" {\n\t\tif err = y.login(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 处理家庭云ID\n\tif y.FamilyID == \"\" {\n\t\tif y.FamilyID, err = y.getFamilyID(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ty.cron = cron.NewCron(time.Minute * 5)\n\ty.cron.Do(y.keepAlive)\n\n\treturn err\n}\n\nfunc (y *Cloud189TV) Drop(ctx context.Context) error {\n\tif y.cron != nil {\n\t\ty.cron.Stop()\n\t\ty.cron = nil\n\t}\n\treturn nil\n}\n\nfunc (y *Cloud189TV) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn y.getFiles(ctx, dir.GetID(), y.isFamily())\n}\n\nfunc (y *Cloud189TV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar downloadUrl struct {\n\t\tURL string `json:\"fileDownloadUrl\"`\n\t}\n\n\tisFamily := y.isFamily()\n\tfullUrl := ApiUrl\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/getFileDownloadUrl.action\"\n\n\t_, err := y.get(fullUrl, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetQueryParam(\"fileId\", file.GetID())\n\t\tif isFamily {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\": y.FamilyID,\n\t\t\t})\n\t\t} else {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"dt\":   \"3\",\n\t\t\t\t\"flag\": \"1\",\n\t\t\t})\n\t\t}\n\t}, &downloadUrl, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 重定向获取真实链接\n\tdownloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, \"&amp;\", \"&\"), \"http://\", \"https://\", 1)\n\tres, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.RawBody().Close()\n\tif res.StatusCode() == 302 {\n\t\tdownloadUrl.URL = res.Header().Get(\"location\")\n\t}\n\n\tlike := &model.Link{\n\t\tURL: downloadUrl.URL,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{base.UserAgent},\n\t\t},\n\t}\n\n\treturn like, nil\n}\n\nfunc (y *Cloud189TV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tfullUrl := ApiUrl\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/createFolder.action\"\n\n\tvar newFolder Cloud189Folder\n\t_, err := y.post(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"folderName\":   dirName,\n\t\t\t\"relativePath\": \"\",\n\t\t})\n\t\tif isFamily {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\": y.FamilyID,\n\t\t\t\t\"parentId\": parentDir.GetID(),\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"parentFolderId\": parentDir.GetID(),\n\t\t\t})\n\t\t}\n\t}, &newFolder, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &newFolder, nil\n}\n\nfunc (y *Cloud189TV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tother := map[string]string{\"targetFileName\": dstDir.GetName()}\n\n\tresp, err := y.CreateBatchTask(\"MOVE\", IF(isFamily, y.FamilyID, \"\"), dstDir.GetID(), other, BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = y.WaitBatchTask(\"MOVE\", resp.TaskID, time.Millisecond*400); err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcObj, nil\n}\n\nfunc (y *Cloud189TV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tqueryParam := make(map[string]string)\n\tfullUrl := ApiUrl\n\tmethod := http.MethodPost\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t\tmethod = http.MethodGet\n\t\tqueryParam[\"familyId\"] = y.FamilyID\n\t}\n\n\tvar newObj model.Obj\n\tswitch f := srcObj.(type) {\n\tcase *Cloud189File:\n\t\tfullUrl += \"/renameFile.action\"\n\t\tqueryParam[\"fileId\"] = srcObj.GetID()\n\t\tqueryParam[\"destFileName\"] = newName\n\t\tnewObj = &Cloud189File{Icon: f.Icon} // 复用预览\n\tcase *Cloud189Folder:\n\t\tfullUrl += \"/renameFolder.action\"\n\t\tqueryParam[\"folderId\"] = srcObj.GetID()\n\t\tqueryParam[\"destFolderName\"] = newName\n\t\tnewObj = &Cloud189Folder{}\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\n\t_, err := y.request(fullUrl, method, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetQueryParams(queryParam)\n\t}, nil, newObj, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn newObj, nil\n}\n\nfunc (y *Cloud189TV) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tisFamily := y.isFamily()\n\tother := map[string]string{\"targetFileName\": dstDir.GetName()}\n\n\tresp, err := y.CreateBatchTask(\"COPY\", IF(isFamily, y.FamilyID, \"\"), dstDir.GetID(), other, BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn y.WaitBatchTask(\"COPY\", resp.TaskID, time.Second)\n}\n\nfunc (y *Cloud189TV) Remove(ctx context.Context, obj model.Obj) error {\n\tisFamily := y.isFamily()\n\n\tresp, err := y.CreateBatchTask(\"DELETE\", IF(isFamily, y.FamilyID, \"\"), \"\", nil, BatchTaskInfo{\n\t\tFileId:   obj.GetID(),\n\t\tFileName: obj.GetName(),\n\t\tIsFolder: BoolToNumber(obj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 批量任务数量限制，过快会导致无法删除\n\treturn y.WaitBatchTask(\"DELETE\", resp.TaskID, time.Millisecond*200)\n}\n\nfunc (y *Cloud189TV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {\n\toverwrite := true\n\tisFamily := y.isFamily()\n\n\t// 响应时间长,按需启用\n\tif y.Addition.RapidUpload && !stream.IsForceStreamUpload() {\n\t\tif newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {\n\t\t\treturn newObj, nil\n\t\t}\n\t}\n\n\treturn y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)\n}\n\nfunc (y *Cloud189TV) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tcapacityInfo, err := y.getCapacityInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar total, used int64\n\tif y.isFamily() {\n\t\ttotal = capacityInfo.FamilyCapacityInfo.TotalSize\n\t\tused = capacityInfo.FamilyCapacityInfo.UsedSize\n\t} else {\n\t\ttotal = capacityInfo.CloudCapacityInfo.TotalSize\n\t\tused = capacityInfo.CloudCapacityInfo.UsedSize\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/189_tv/help.go",
    "content": "package _189_tv\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc clientSuffix() map[string]string {\n\treturn map[string]string{\n\t\t\"clientType\":        AndroidTV,\n\t\t\"version\":           TvVersion,\n\t\t\"channelId\":         TvChannelId,\n\t\t\"clientSn\":          \"unknown\",\n\t\t\"model\":             \"PJX110\",\n\t\t\"osFamily\":          \"Android\",\n\t\t\"osVersion\":         \"35\",\n\t\t\"networkAccessMode\": \"WIFI\",\n\t\t\"telecomsOperator\":  \"46011\",\n\t}\n}\n\n// SessionKeySignatureOfHmac HMAC签名\nfunc SessionKeySignatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]\n\tmac := hmac.New(sha1.New, []byte(sessionSecret))\n\tdata := fmt.Sprintf(\"SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s\", sessionKey, operate, urlpath, dateOfGmt)\n\tmac.Write([]byte(data))\n\treturn strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))\n}\n\n// AppKeySignatureOfHmac HMAC签名\nfunc AppKeySignatureOfHmac(sessionSecret, appKey, operate, fullUrl string, timestamp int64) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]\n\tmac := hmac.New(sha1.New, []byte(sessionSecret))\n\tdata := fmt.Sprintf(\"AppKey=%s&Operate=%s&RequestURI=%s&Timestamp=%d\", appKey, operate, urlpath, timestamp)\n\tmac.Write([]byte(data))\n\treturn strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))\n}\n\n// 获取http规范的时间\nfunc getHttpDateStr() string {\n\treturn time.Now().UTC().Format(http.TimeFormat)\n}\n\n// 时间戳\nfunc timestamp() int64 {\n\treturn time.Now().UTC().UnixNano() / 1e6\n}\n\ntype Time time.Time\n\nfunc (t *Time) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }\nfunc (t *Time) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {\n\tb, err := e.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif b, ok := b.(xml.CharData); ok {\n\t\tif err = t.Unmarshal(b); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn e.Skip()\n}\nfunc (t *Time) Unmarshal(b []byte) error {\n\tbs := strings.Trim(string(b), \"\\\"\")\n\tvar v time.Time\n\tvar err error\n\tfor _, f := range []string{\"2006-01-02 15:04:05 -07\", \"Jan 2, 2006 15:04:05 PM -07\"} {\n\t\tv, err = time.ParseInLocation(f, bs+\" +08\", time.Local)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\t*t = Time(v)\n\treturn err\n}\n\ntype String string\n\nfunc (t *String) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }\nfunc (t *String) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {\n\tb, err := e.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif b, ok := b.(xml.CharData); ok {\n\t\tif err = t.Unmarshal(b); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn e.Skip()\n}\nfunc (s *String) Unmarshal(b []byte) error {\n\t*s = String(bytes.Trim(b, \"\\\"\"))\n\treturn nil\n}\n\nfunc toFamilyOrderBy(o string) string {\n\tswitch o {\n\tcase \"filename\":\n\t\treturn \"1\"\n\tcase \"filesize\":\n\t\treturn \"2\"\n\tcase \"lastOpTime\":\n\t\treturn \"3\"\n\tdefault:\n\t\treturn \"1\"\n\t}\n}\n\nfunc toDesc(o string) string {\n\tswitch o {\n\tcase \"desc\":\n\t\treturn \"true\"\n\tcase \"asc\":\n\t\tfallthrough\n\tdefault:\n\t\treturn \"false\"\n\t}\n}\n\nfunc ParseHttpHeader(str string) map[string]string {\n\theader := make(map[string]string)\n\tfor _, value := range strings.Split(str, \"&\") {\n\t\tif k, v, found := strings.Cut(value, \"=\"); found {\n\t\t\theader[k] = v\n\t\t}\n\t}\n\treturn header\n}\n\nfunc MustString(str string, err error) string {\n\treturn str\n}\n\nfunc BoolToNumber(b bool) int {\n\tif b {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc isBool(bs ...bool) bool {\n\tfor _, b := range bs {\n\t\tif b {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc IF[V any](o bool, t V, f V) V {\n\tif o {\n\t\treturn t\n\t}\n\treturn f\n}\n"
  },
  {
    "path": "drivers/189_tv/meta.go",
    "content": "package _189_tv\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tAccessToken    string `json:\"access_token\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"filename,filesize,lastOpTime\" default:\"filename\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tType           string `json:\"type\" type:\"select\" options:\"personal,family\" default:\"personal\"`\n\tFamilyID       string `json:\"family_id\"`\n\tUploadThread   string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\n\tRapidUpload    bool   `json:\"rapid_upload\"`\n}\n\nvar config = driver.Config{\n\tName:        \"189CloudTV\",\n\tDefaultRoot: \"-11\",\n\tCheckStatus: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Cloud189TV{}\n\t})\n}\n"
  },
  {
    "path": "drivers/189_tv/types.go",
    "content": "package _189_tv\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// 居然有四种返回方式\ntype RespErr struct {\n\tResCode    any    `json:\"res_code\"` // int or string\n\tResMessage string `json:\"res_message\"`\n\n\tError_ string `json:\"error\"`\n\n\tXMLName xml.Name `xml:\"error\"`\n\tCode    string   `json:\"code\" xml:\"code\"`\n\tMessage string   `json:\"message\" xml:\"message\"`\n\tMsg     string   `json:\"msg\"`\n\n\tErrorCode string `json:\"errorCode\"`\n\tErrorMsg  string `json:\"errorMsg\"`\n}\n\nfunc (e *RespErr) HasError() bool {\n\tswitch v := e.ResCode.(type) {\n\tcase int, int64, int32:\n\t\treturn v != 0\n\tcase string:\n\t\treturn e.ResCode != \"\"\n\t}\n\treturn (e.Code != \"\" && e.Code != \"SUCCESS\") || e.ErrorCode != \"\" || e.Error_ != \"\"\n}\n\nfunc (e *RespErr) Error() string {\n\tswitch v := e.ResCode.(type) {\n\tcase int, int64, int32:\n\t\tif v != 0 {\n\t\t\treturn fmt.Sprintf(\"res_code: %d ,res_msg: %s\", v, e.ResMessage)\n\t\t}\n\tcase string:\n\t\tif e.ResCode != \"\" {\n\t\t\treturn fmt.Sprintf(\"res_code: %s ,res_msg: %s\", e.ResCode, e.ResMessage)\n\t\t}\n\t}\n\n\tif e.Code != \"\" && e.Code != \"SUCCESS\" {\n\t\tif e.Msg != \"\" {\n\t\t\treturn fmt.Sprintf(\"code: %s ,msg: %s\", e.Code, e.Msg)\n\t\t}\n\t\tif e.Message != \"\" {\n\t\t\treturn fmt.Sprintf(\"code: %s ,msg: %s\", e.Code, e.Message)\n\t\t}\n\t\treturn \"code: \" + e.Code\n\t}\n\n\tif e.ErrorCode != \"\" {\n\t\treturn fmt.Sprintf(\"err_code: %s ,err_msg: %s\", e.ErrorCode, e.ErrorMsg)\n\t}\n\n\tif e.Error_ != \"\" {\n\t\treturn fmt.Sprintf(\"error: %s ,message: %s\", e.ErrorCode, e.Message)\n\t}\n\treturn \"\"\n}\n\n// 刷新session返回\ntype UserSessionResp struct {\n\tResCode    int    `json:\"res_code\"`\n\tResMessage string `json:\"res_message\"`\n\n\tLoginName string `json:\"loginName\"`\n\n\tKeepAlive       int `json:\"keepAlive\"`\n\tGetFileDiffSpan int `json:\"getFileDiffSpan\"`\n\tGetUserInfoSpan int `json:\"getUserInfoSpan\"`\n\n\t// 个人云\n\tSessionKey    string `json:\"sessionKey\"`\n\tSessionSecret string `json:\"sessionSecret\"`\n\t// 家庭云\n\tFamilySessionKey    string `json:\"familySessionKey\"`\n\tFamilySessionSecret string `json:\"familySessionSecret\"`\n}\n\ntype UuidInfoResp struct {\n\tUuid string `json:\"uuid\"`\n}\n\ntype E189AccessTokenResp struct {\n\tE189AccessToken string `json:\"accessToken\"`\n\tExpiresIn       int64  `json:\"expiresIn\"`\n}\n\n// 登录返回\ntype AppSessionResp struct {\n\tUserSessionResp\n\n\tIsSaveName string `json:\"isSaveName\"`\n\n\t// 会话刷新Token\n\tAccessToken string `json:\"accessToken\"`\n\t//Token刷新\n\tRefreshToken string `json:\"refreshToken\"`\n}\n\n// 家庭云账户\ntype FamilyInfoListResp struct {\n\tFamilyInfoResp []FamilyInfoResp `json:\"familyInfoResp\"`\n}\ntype FamilyInfoResp struct {\n\tCount      int    `json:\"count\"`\n\tCreateTime string `json:\"createTime\"`\n\tFamilyID   int64  `json:\"familyId\"`\n\tRemarkName string `json:\"remarkName\"`\n\tType       int    `json:\"type\"`\n\tUseFlag    int    `json:\"useFlag\"`\n\tUserRole   int    `json:\"userRole\"`\n}\n\n/*文件部分*/\n// 文件\ntype Cloud189File struct {\n\tID   String `json:\"id\"`\n\tName string `json:\"name\"`\n\tSize int64  `json:\"size\"`\n\tMd5  string `json:\"md5\"`\n\n\tLastOpTime Time `json:\"lastOpTime\"`\n\tCreateDate Time `json:\"createDate\"`\n\tIcon       struct {\n\t\t//iconOption 5\n\t\tSmallUrl string `json:\"smallUrl\"`\n\t\tLargeUrl string `json:\"largeUrl\"`\n\n\t\t// iconOption 10\n\t\tMax600    string `json:\"max600\"`\n\t\tMediumURL string `json:\"mediumUrl\"`\n\t} `json:\"icon\"`\n\n\t// Orientation int64  `json:\"orientation\"`\n\t// FileCata   int64  `json:\"fileCata\"`\n\t// MediaType   int    `json:\"mediaType\"`\n\t// Rev         string `json:\"rev\"`\n\t// StarLabel   int64  `json:\"starLabel\"`\n}\n\nfunc (c *Cloud189File) CreateTime() time.Time {\n\treturn time.Time(c.CreateDate)\n}\n\nfunc (c *Cloud189File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.MD5, c.Md5)\n}\n\nfunc (c *Cloud189File) GetSize() int64     { return c.Size }\nfunc (c *Cloud189File) GetName() string    { return c.Name }\nfunc (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }\nfunc (c *Cloud189File) IsDir() bool        { return false }\nfunc (c *Cloud189File) GetID() string      { return string(c.ID) }\nfunc (c *Cloud189File) GetPath() string    { return \"\" }\nfunc (c *Cloud189File) Thumb() string      { return c.Icon.SmallUrl }\n\n// 文件夹\ntype Cloud189Folder struct {\n\tID       String `json:\"id\"`\n\tParentID int64  `json:\"parentId\"`\n\tName     string `json:\"name\"`\n\n\tLastOpTime Time `json:\"lastOpTime\"`\n\tCreateDate Time `json:\"createDate\"`\n\n\t// FileListSize int64 `json:\"fileListSize\"`\n\t// FileCount int64 `json:\"fileCount\"`\n\t// FileCata  int64 `json:\"fileCata\"`\n\t// Rev          string `json:\"rev\"`\n\t// StarLabel    int64  `json:\"starLabel\"`\n}\n\nfunc (c *Cloud189Folder) CreateTime() time.Time {\n\treturn time.Time(c.CreateDate)\n}\n\nfunc (c *Cloud189Folder) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (c *Cloud189Folder) GetSize() int64     { return 0 }\nfunc (c *Cloud189Folder) GetName() string    { return c.Name }\nfunc (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }\nfunc (c *Cloud189Folder) IsDir() bool        { return true }\nfunc (c *Cloud189Folder) GetID() string      { return string(c.ID) }\nfunc (c *Cloud189Folder) GetPath() string    { return \"\" }\n\ntype Cloud189FilesResp struct {\n\t//ResCode    int    `json:\"res_code\"`\n\t//ResMessage string `json:\"res_message\"`\n\tFileListAO struct {\n\t\tCount      int              `json:\"count\"`\n\t\tFileList   []Cloud189File   `json:\"fileList\"`\n\t\tFolderList []Cloud189Folder `json:\"folderList\"`\n\t} `json:\"fileListAO\"`\n}\n\n// TaskInfo 任务信息\ntype BatchTaskInfo struct {\n\t// FileId 文件ID\n\tFileId string `json:\"fileId\"`\n\t// FileName 文件名\n\tFileName string `json:\"fileName\"`\n\t// IsFolder 是否是文件夹，0-否，1-是\n\tIsFolder int `json:\"isFolder\"`\n\t// SrcParentId 文件所在父目录ID\n\tSrcParentId string `json:\"srcParentId,omitempty\"`\n\n\t/* 冲突管理 */\n\t// 1 -> 跳过 2 -> 保留 3 -> 覆盖\n\tDealWay    int `json:\"dealWay,omitempty\"`\n\tIsConflict int `json:\"isConflict,omitempty\"`\n}\n\n/* 上传部分 */\ntype InitMultiUploadResp struct {\n\t//Code string `json:\"code\"`\n\tData struct {\n\t\tUploadType     int    `json:\"uploadType\"`\n\t\tUploadHost     string `json:\"uploadHost\"`\n\t\tUploadFileID   string `json:\"uploadFileId\"`\n\t\tFileDataExists int    `json:\"fileDataExists\"`\n\t} `json:\"data\"`\n}\ntype UploadUrlsResp struct {\n\tCode string                    `json:\"code\"`\n\tData map[string]UploadUrlsData `json:\"uploadUrls\"`\n}\ntype UploadUrlsData struct {\n\tRequestURL    string `json:\"requestURL\"`\n\tRequestHeader string `json:\"requestHeader\"`\n}\n\n/* 第二种上传方式 */\ntype CreateUploadFileResp struct {\n\t// 上传文件请求ID\n\tUploadFileId int64 `json:\"uploadFileId\"`\n\t// 上传文件数据的URL路径\n\tFileUploadUrl string `json:\"fileUploadUrl\"`\n\t// 上传文件完成后确认路径\n\tFileCommitUrl string `json:\"fileCommitUrl\"`\n\t// 文件是否已存在云盘中，0-未存在，1-已存在\n\tFileDataExists int `json:\"fileDataExists\"`\n}\n\ntype GetUploadFileStatusResp struct {\n\tCreateUploadFileResp\n\n\t// 已上传的大小\n\tDataSize int64 `json:\"dataSize\"`\n\tSize     int64 `json:\"size\"`\n}\n\nfunc (r *GetUploadFileStatusResp) GetSize() int64 {\n\treturn r.DataSize + r.Size\n}\n\ntype CommitMultiUploadFileResp struct {\n\tFile struct {\n\t\tUserFileID String `json:\"userFileId\"`\n\t\tFileName   string `json:\"fileName\"`\n\t\tFileSize   int64  `json:\"fileSize\"`\n\t\tFileMd5    string `json:\"fileMd5\"`\n\t\tCreateDate Time   `json:\"createDate\"`\n\t} `json:\"file\"`\n}\n\ntype OldCommitUploadFileResp struct {\n\tXMLName    xml.Name `xml:\"file\"`\n\tID         String   `xml:\"id\"`\n\tName       string   `xml:\"name\"`\n\tSize       int64    `xml:\"size\"`\n\tMd5        string   `xml:\"md5\"`\n\tCreateDate Time     `xml:\"createDate\"`\n}\n\nfunc (f *OldCommitUploadFileResp) toFile() *Cloud189File {\n\treturn &Cloud189File{\n\t\tID:         f.ID,\n\t\tName:       f.Name,\n\t\tSize:       f.Size,\n\t\tMd5:        f.Md5,\n\t\tCreateDate: f.CreateDate,\n\t\tLastOpTime: f.CreateDate,\n\t}\n}\n\ntype CreateBatchTaskResp struct {\n\tTaskID string `json:\"taskId\"`\n}\n\ntype BatchTaskStateResp struct {\n\tFailedCount         int     `json:\"failedCount\"`\n\tProcess             int     `json:\"process\"`\n\tSkipCount           int     `json:\"skipCount\"`\n\tSubTaskCount        int     `json:\"subTaskCount\"`\n\tSuccessedCount      int     `json:\"successedCount\"`\n\tSuccessedFileIDList []int64 `json:\"successedFileIdList\"`\n\tTaskID              string  `json:\"taskId\"`\n\tTaskStatus          int     `json:\"taskStatus\"` //1 初始化 2 存在冲突 3 执行中，4 完成\n}\n\ntype BatchTaskConflictTaskInfoResp struct {\n\tSessionKey     string `json:\"sessionKey\"`\n\tTargetFolderID int    `json:\"targetFolderId\"`\n\tTaskID         string `json:\"taskId\"`\n\tTaskInfos      []BatchTaskInfo\n\tTaskType       int `json:\"taskType\"`\n}\n\ntype CapacityResp struct {\n\tResCode           int    `json:\"res_code\"`\n\tResMessage        string `json:\"res_message\"`\n\tAccount           string `json:\"account\"`\n\tCloudCapacityInfo struct {\n\t\tFreeSize     int64 `json:\"freeSize\"`\n\t\tMailUsedSize int64 `json:\"mail189UsedSize\"`\n\t\tTotalSize    int64 `json:\"totalSize\"`\n\t\tUsedSize     int64 `json:\"usedSize\"`\n\t} `json:\"cloudCapacityInfo\"`\n\tFamilyCapacityInfo struct {\n\t\tFreeSize  int64 `json:\"freeSize\"`\n\t\tTotalSize int64 `json:\"totalSize\"`\n\t\tUsedSize  int64 `json:\"usedSize\"`\n\t} `json:\"familyCapacityInfo\"`\n\tTotalSize uint64 `json:\"totalSize\"`\n}\n"
  },
  {
    "path": "drivers/189_tv/utils.go",
    "content": "package _189_tv\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/skip2/go-qrcode\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/pkg/errors\"\n)\n\nconst (\n\tTVAppKey            = \"600100885\"\n\tTVAppSignatureSecre = \"fe5734c74c2f96a38157f420b32dc995\"\n\tTvVersion           = \"6.5.5\"\n\tAndroidTV           = \"FAMILY_TV\"\n\tTvChannelId         = \"home02\"\n\n\tApiUrl = \"https://api.cloud.189.cn\"\n)\n\nfunc (y *Cloud189TV) SignatureHeader(url, method string, isFamily bool) map[string]string {\n\tdateOfGmt := getHttpDateStr()\n\tsessionKey := y.tokenInfo.SessionKey\n\tsessionSecret := y.tokenInfo.SessionSecret\n\tif isFamily {\n\t\tsessionKey = y.tokenInfo.FamilySessionKey\n\t\tsessionSecret = y.tokenInfo.FamilySessionSecret\n\t}\n\n\theader := map[string]string{\n\t\t\"Date\":         dateOfGmt,\n\t\t\"SessionKey\":   sessionKey,\n\t\t\"X-Request-ID\": uuid.NewString(),\n\t\t\"Signature\":    SessionKeySignatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt),\n\t}\n\treturn header\n}\n\nfunc (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string {\n\ttempTime := timestamp()\n\theader := map[string]string{\n\t\t\"Timestamp\":    strconv.FormatInt(tempTime, 10),\n\t\t\"X-Request-ID\": uuid.NewString(),\n\t\t\"AppKey\":       TVAppKey,\n\t\t\"AppSignature\": AppKeySignatureOfHmac(TVAppSignatureSecre, TVAppKey, method, url, tempTime),\n\t}\n\treturn header\n}\n\nfunc (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {\n\treturn y.requestWithRetry(url, method, callback, params, resp, 0, isFamily...)\n}\n\nfunc (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) {\n\tif y.tokenInfo == nil {\n\t\treturn nil, fmt.Errorf(\"login failed\")\n\t}\n\treq := y.client.R().SetQueryParams(clientSuffix())\n\n\tif params != nil {\n\t\treq.SetQueryParams(params)\n\t}\n\n\t// Signature\n\treq.SetHeaders(y.SignatureHeader(url, method, isBool(isFamily...)))\n\n\tvar erron RespErr\n\treq.SetError(&erron)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif strings.Contains(res.String(), \"userSessionBO is null\") ||\n\t\tstrings.Contains(res.String(), \"InvalidSessionKey\") {\n\t\t// 限制重试次数，避免无限递归\n\t\tif retryCount >= 3 {\n\t\t\ty.Addition.AccessToken = \"\"\n\t\t\top.MustSaveDriverStorage(y)\n\t\t\treturn nil, errors.New(\"session expired after retry\")\n\t\t}\n\n\t\t// 尝试刷新会话\n\t\tif err := y.refreshSession(); err != nil {\n\t\t\t// 如果刷新失败，说明AccessToken也已过期，需要重新登录\n\t\t\ty.Addition.AccessToken = \"\"\n\t\t\top.MustSaveDriverStorage(y)\n\t\t\treturn nil, errors.New(\"session expired\")\n\t\t}\n\t\t// 如果刷新成功，则重试原始请求（增加重试计数）\n\t\treturn y.requestWithRetry(url, method, callback, params, resp, retryCount+1, isFamily...)\n\t}\n\n\t// 处理错误\n\tif erron.HasError() {\n\t\treturn nil, &erron\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (y *Cloud189TV) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {\n\treturn y.request(url, http.MethodGet, callback, nil, resp, isFamily...)\n}\n\nfunc (y *Cloud189TV) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {\n\treturn y.request(url, http.MethodPost, callback, nil, resp, isFamily...)\n}\n\nfunc (y *Cloud189TV) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := req.URL.Query()\n\tfor key, value := range clientSuffix() {\n\t\tquery.Add(key, value)\n\t}\n\treq.URL.RawQuery = query.Encode()\n\n\tfor key, value := range headers {\n\t\treq.Header.Add(key, value)\n\t}\n\n\tif sign {\n\t\tfor key, value := range y.SignatureHeader(url, http.MethodPut, isFamily) {\n\t\t\treq.Header.Add(key, value)\n\t\t}\n\t}\n\n\t// 请求完成后http.Client会Close Request.Body\n\tresp, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar erron RespErr\n\tjsoniter.Unmarshal(body, &erron)\n\txml.Unmarshal(body, &erron)\n\tif erron.HasError() {\n\t\treturn nil, &erron\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Errorf(\"put fail,err:%s\", string(body))\n\t}\n\treturn body, nil\n}\n\nfunc (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {\n\tfullUrl := ApiUrl\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/listFiles.action\"\n\n\tres := make([]model.Obj, 0, 130)\n\tfor pageNum := 1; ; pageNum++ {\n\t\tvar resp Cloud189FilesResp\n\t\t_, err := y.get(fullUrl, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"folderId\":   fileId,\n\t\t\t\t\"fileType\":   \"0\",\n\t\t\t\t\"mediaAttr\":  \"0\",\n\t\t\t\t\"iconOption\": \"5\",\n\t\t\t\t\"pageNum\":    fmt.Sprint(pageNum),\n\t\t\t\t\"pageSize\":   \"130\",\n\t\t\t})\n\t\t\tif isFamily {\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"familyId\":   y.FamilyID,\n\t\t\t\t\t\"orderBy\":    toFamilyOrderBy(y.OrderBy),\n\t\t\t\t\t\"descending\": toDesc(y.OrderDirection),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"recursive\":  \"0\",\n\t\t\t\t\t\"orderBy\":    y.OrderBy,\n\t\t\t\t\t\"descending\": toDesc(y.OrderDirection),\n\t\t\t\t})\n\t\t\t}\n\t\t}, &resp, isFamily)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 获取完毕跳出\n\t\tif resp.FileListAO.Count == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tfor i := 0; i < len(resp.FileListAO.FolderList); i++ {\n\t\t\tres = append(res, &resp.FileListAO.FolderList[i])\n\t\t}\n\t\tfor i := 0; i < len(resp.FileListAO.FileList); i++ {\n\t\t\tres = append(res, &resp.FileListAO.FileList[i])\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (y *Cloud189TV) login() (err error) {\n\treq := y.client.R().SetQueryParams(clientSuffix())\n\tvar erron RespErr\n\tvar tokenInfo AppSessionResp\n\tif y.Addition.AccessToken == \"\" {\n\t\tif y.TempUuid == \"\" {\n\t\t\t// 获取登录参数\n\t\t\tvar uuidInfo UuidInfoResp\n\t\t\treq.SetResult(&uuidInfo).SetError(&erron)\n\t\t\t// Signature\n\t\t\treq.SetHeaders(y.AppKeySignatureHeader(ApiUrl+\"/family/manage/getQrCodeUUID.action\",\n\t\t\t\thttp.MethodGet))\n\t\t\t_, err = req.Execute(http.MethodGet, ApiUrl+\"/family/manage/getQrCodeUUID.action\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif erron.HasError() {\n\t\t\t\treturn &erron\n\t\t\t}\n\n\t\t\tif uuidInfo.Uuid == \"\" {\n\t\t\t\treturn errors.New(\"uuidInfo is empty\")\n\t\t\t}\n\t\t\ty.TempUuid = uuidInfo.Uuid\n\t\t\top.MustSaveDriverStorage(y)\n\n\t\t\t// 展示二维码\n\t\t\tqrTemplate := `<body>\n    <img src=\"data:image/jpeg;base64,%s\"/>\n    <br>Or Click here: <a href=\"%s\">%s</a>\n</body>`\n\n\t\t\t// Generate QR code\n\t\t\tqrCode, err := qrcode.Encode(uuidInfo.Uuid, qrcode.Medium, 256)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to generate QR code: %v\", err)\n\t\t\t}\n\n\t\t\t// Encode QR code to base64\n\t\t\tqrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)\n\n\t\t\t// Create the HTML page\n\t\t\tqrPage := fmt.Sprintf(qrTemplate, qrCodeBase64, uuidInfo.Uuid, uuidInfo.Uuid)\n\t\t\treturn fmt.Errorf(\"need verify: \\n%s\", qrPage)\n\n\t\t} else {\n\t\t\tvar accessTokenResp E189AccessTokenResp\n\t\t\treq.SetResult(&accessTokenResp).SetError(&erron)\n\t\t\t// Signature\n\t\t\treq.SetHeaders(y.AppKeySignatureHeader(ApiUrl+\"/family/manage/qrcodeLoginResult.action\",\n\t\t\t\thttp.MethodGet))\n\t\t\treq.SetQueryParam(\"uuid\", y.TempUuid)\n\t\t\t_, err = req.Execute(http.MethodGet, ApiUrl+\"/family/manage/qrcodeLoginResult.action\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif erron.HasError() {\n\t\t\t\treturn &erron\n\t\t\t}\n\t\t\tif accessTokenResp.E189AccessToken == \"\" {\n\t\t\t\treturn errors.New(\"E189AccessToken is empty\")\n\t\t\t}\n\t\t\ty.Addition.AccessToken = accessTokenResp.E189AccessToken\n\t\t}\n\t}\n\t// 获取SessionKey 和 SessionSecret\n\treqb := y.client.R().SetQueryParams(clientSuffix())\n\treqb.SetResult(&tokenInfo).SetError(&erron)\n\t// Signature\n\treqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+\"/family/manage/loginFamilyMerge.action\",\n\t\thttp.MethodGet))\n\treqb.SetQueryParam(\"e189AccessToken\", y.Addition.AccessToken)\n\t_, err = reqb.Execute(http.MethodGet, ApiUrl+\"/family/manage/loginFamilyMerge.action\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif erron.HasError() {\n\t\treturn &erron\n\t}\n\n\ty.tokenInfo = &tokenInfo\n\top.MustSaveDriverStorage(y)\n\treturn err\n}\n\n// refreshSession 尝试使用现有的 AccessToken 刷新会话\nfunc (y *Cloud189TV) refreshSession() (err error) {\n\tvar erron RespErr\n\tvar tokenInfo AppSessionResp\n\treqb := y.client.R().SetQueryParams(clientSuffix())\n\treqb.SetResult(&tokenInfo).SetError(&erron)\n\t// Signature\n\treqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+\"/family/manage/loginFamilyMerge.action\",\n\t\thttp.MethodGet))\n\treqb.SetQueryParam(\"e189AccessToken\", y.Addition.AccessToken)\n\t_, err = reqb.Execute(http.MethodGet, ApiUrl+\"/family/manage/loginFamilyMerge.action\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif erron.HasError() {\n\t\treturn &erron\n\t}\n\n\ty.tokenInfo = &tokenInfo\n\treturn nil\n}\n\nfunc (y *Cloud189TV) keepAlive() {\n\t_, err := y.get(ApiUrl+\"/keepUserSession.action\", func(r *resty.Request) {\n\t\tr.SetQueryParams(clientSuffix())\n\t}, nil)\n\tif err != nil {\n\t\tutils.Log.Warnf(\"189tv: Failed to keep user session alive: %v\", err)\n\t\t// 如果keepAlive失败，尝试刷新session\n\t\tif refreshErr := y.refreshSession(); refreshErr != nil {\n\t\t\tutils.Log.Errorf(\"189tv: Failed to refresh session after keepAlive error: %v\", refreshErr)\n\t\t}\n\t} else {\n\t\tutils.Log.Debugf(\"189tv: User session kept alive successfully.\")\n\t}\n}\n\nfunc (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {\n\tfileMd5 := stream.GetHash().GetHash(utils.MD5)\n\tif len(fileMd5) < utils.MD5.Width {\n\t\treturn nil, errors.New(\"invalid hash\")\n\t}\n\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif uploadInfo.FileDataExists != 1 {\n\t\treturn nil, errors.New(\"rapid upload fail\")\n\t}\n\n\treturn y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)\n}\n\n// 旧版本上传，家庭云不支持覆盖\nfunc (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {\n\tfileMd5 := file.GetHash().GetHash(utils.MD5)\n\ttempFile := file.GetFile()\n\tvar err error\n\tif len(fileMd5) != utils.MD5.Width {\n\t\ttempFile, fileMd5, err = stream.CacheFullAndHash(file, &up, utils.MD5)\n\t} else if tempFile == nil {\n\t\ttempFile, err = file.CacheFullAndWriter(&up, nil)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 创建上传会话\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 网盘中不存在该文件，开始上传\n\tstatus := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}\n\t// driver.RateLimitReader会尝试Close底层的reader\n\t// 但这里的tempFile是一个*os.File，Close后就没法继续读了\n\t// 所以这里用io.NopCloser包一层\n\trateLimitedRd := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))\n\tfor status.GetSize() < file.GetSize() && status.FileDataExists != 1 {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\n\t\theader := map[string]string{\n\t\t\t\"ResumePolicy\": \"1\",\n\t\t\t\"Expect\":       \"100-continue\",\n\t\t}\n\n\t\tif isFamily {\n\t\t\theader[\"FamilyId\"] = fmt.Sprint(y.FamilyID)\n\t\t\theader[\"UploadFileId\"] = fmt.Sprint(status.UploadFileId)\n\t\t} else {\n\t\t\theader[\"Edrive-UploadFileId\"] = fmt.Sprint(status.UploadFileId)\n\t\t}\n\n\t\t_, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimitedRd, isFamily)\n\t\tif err, ok := err.(*RespErr); ok && err.Code != \"InputStreamReadError\" {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 获取断点状态\n\t\tfullUrl := ApiUrl + \"/getUploadFileStatus.action\"\n\t\tif y.isFamily() {\n\t\t\tfullUrl = ApiUrl + \"/family/file/getFamilyFileStatus.action\"\n\t\t}\n\t\t_, err = y.get(fullUrl, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx).SetQueryParams(map[string]string{\n\t\t\t\t\"uploadFileId\": fmt.Sprint(status.UploadFileId),\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t})\n\t\t\tif isFamily {\n\t\t\t\treq.SetQueryParam(\"familyId\", fmt.Sprint(y.FamilyID))\n\t\t\t}\n\t\t}, &status, isFamily)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tup(float64(status.GetSize()) / float64(file.GetSize()) * 100)\n\t}\n\n\treturn y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)\n}\n\n// 创建上传会话\nfunc (y *Cloud189TV) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {\n\tvar uploadInfo CreateUploadFileResp\n\n\tfullUrl := ApiUrl + \"/createUploadFile.action\"\n\tif isFamily {\n\t\tfullUrl = ApiUrl + \"/family/file/createFamilyFile.action\"\n\t}\n\t_, err := y.post(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\tif isFamily {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\":     y.FamilyID,\n\t\t\t\t\"parentId\":     parentID,\n\t\t\t\t\"fileMd5\":      fileMd5,\n\t\t\t\t\"fileName\":     fileName,\n\t\t\t\t\"fileSize\":     fileSize,\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"parentFolderId\": parentID,\n\t\t\t\t\"fileName\":       fileName,\n\t\t\t\t\"size\":           fileSize,\n\t\t\t\t\"md5\":            fileMd5,\n\t\t\t\t\"opertype\":       \"3\",\n\t\t\t\t\"flag\":           \"1\",\n\t\t\t\t\"resumePolicy\":   \"1\",\n\t\t\t\t\"isLog\":          \"0\",\n\t\t\t})\n\t\t}\n\t}, &uploadInfo, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &uploadInfo, nil\n}\n\n// 提交上传文件\nfunc (y *Cloud189TV) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {\n\tvar resp OldCommitUploadFileResp\n\t_, err := y.post(fileCommitUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\tif isFamily {\n\t\t\treq.SetHeaders(map[string]string{\n\t\t\t\t\"ResumePolicy\": \"1\",\n\t\t\t\t\"UploadFileId\": fmt.Sprint(uploadFileID),\n\t\t\t\t\"FamilyId\":     fmt.Sprint(y.FamilyID),\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"opertype\":     IF(overwrite, \"3\", \"1\"),\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t\t\"uploadFileId\": fmt.Sprint(uploadFileID),\n\t\t\t\t\"isLog\":        \"0\",\n\t\t\t})\n\t\t}\n\t}, &resp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.toFile(), nil\n}\n\nfunc (y *Cloud189TV) isFamily() bool {\n\treturn y.Type == \"family\"\n}\n\nfunc (y *Cloud189TV) isLogin() bool {\n\tif y.tokenInfo == nil {\n\t\treturn false\n\t}\n\t_, err := y.get(ApiUrl+\"/getUserInfo.action\", nil, nil)\n\treturn err == nil\n}\n\n// 获取家庭云所有用户信息\nfunc (y *Cloud189TV) getFamilyInfoList() ([]FamilyInfoResp, error) {\n\tvar resp FamilyInfoListResp\n\t_, err := y.get(ApiUrl+\"/family/manage/getFamilyList.action\", nil, &resp, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.FamilyInfoResp, nil\n}\n\n// 抽取家庭云ID\nfunc (y *Cloud189TV) getFamilyID() (string, error) {\n\tinfos, err := y.getFamilyInfoList()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(infos) == 0 {\n\t\treturn \"\", fmt.Errorf(\"cannot get automatically,please input family_id\")\n\t}\n\tfor _, info := range infos {\n\t\tif strings.Contains(y.tokenInfo.LoginName, info.RemarkName) {\n\t\t\treturn fmt.Sprint(info.FamilyID), nil\n\t\t}\n\t}\n\treturn fmt.Sprint(infos[0].FamilyID), nil\n}\n\nfunc (y *Cloud189TV) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {\n\tvar resp CreateBatchTaskResp\n\t_, err := y.post(ApiUrl+\"/batch/createBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":      aType,\n\t\t\t\"taskInfos\": MustString(utils.Json.MarshalToString(taskInfos)),\n\t\t})\n\t\tif targetFolderId != \"\" {\n\t\t\treq.SetFormData(map[string]string{\"targetFolderId\": targetFolderId})\n\t\t}\n\t\tif familyID != \"\" {\n\t\t\treq.SetFormData(map[string]string{\"familyId\": familyID})\n\t\t}\n\t\treq.SetFormData(other)\n\t}, &resp, familyID != \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 检测任务状态\nfunc (y *Cloud189TV) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {\n\tvar resp BatchTaskStateResp\n\t_, err := y.post(ApiUrl+\"/batch/checkBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":   aType,\n\t\t\t\"taskId\": taskID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 获取冲突的任务信息\nfunc (y *Cloud189TV) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {\n\tvar resp BatchTaskConflictTaskInfoResp\n\t_, err := y.post(ApiUrl+\"/batch/getConflictTaskInfo.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":   aType,\n\t\t\t\"taskId\": taskID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 处理冲突\nfunc (y *Cloud189TV) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {\n\t_, err := y.post(ApiUrl+\"/batch/manageBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"targetFolderId\": targetFolderId,\n\t\t\t\"type\":           aType,\n\t\t\t\"taskId\":         taskID,\n\t\t\t\"taskInfos\":      MustString(utils.Json.MarshalToString(taskInfos)),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nvar ErrIsConflict = errors.New(\"there is a conflict with the target object\")\n\n// 等待任务完成\nfunc (y *Cloud189TV) WaitBatchTask(aType string, taskID string, t time.Duration) error {\n\tfor {\n\t\tstate, err := y.CheckBatchTask(aType, taskID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch state.TaskStatus {\n\t\tcase 2:\n\t\t\treturn ErrIsConflict\n\t\tcase 4:\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(t)\n\t}\n}\n\nfunc (y *Cloud189TV) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {\n\tfullUrl := ApiUrl + \"/portal/getUserSizeInfo.action\"\n\tvar resp CapacityResp\n\t_, err := y.get(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/189pc/driver.go",
    "content": "package _189pc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n)\n\ntype Cloud189PC struct {\n\tmodel.Storage\n\tAddition\n\n\tclient *resty.Client\n\n\tloginParam  *LoginParam\n\tqrcodeParam *QRLoginParam\n\n\ttokenInfo *AppSessionResp\n\n\tuploadThread int\n\n\tfamilyTransferFolder    *Cloud189Folder\n\tcleanFamilyTransferFile func()\n\n\tstorageConfig driver.Config\n\tref           *Cloud189PC\n\tcron          *cron.Cron\n}\n\nfunc (y *Cloud189PC) Config() driver.Config {\n\tif y.storageConfig.Name == \"\" {\n\t\ty.storageConfig = config\n\t}\n\treturn y.storageConfig\n}\n\nfunc (y *Cloud189PC) GetAddition() driver.Additional {\n\treturn &y.Addition\n}\n\nfunc (y *Cloud189PC) Init(ctx context.Context) (err error) {\n\ty.storageConfig = config\n\tif y.isFamily() {\n\t\t// 兼容旧上传接口\n\t\tif y.Addition.RapidUpload || y.Addition.UploadMethod == \"old\" {\n\t\t\ty.storageConfig.NoOverwriteUpload = true\n\t\t}\n\t} else {\n\t\t// 家庭云转存，不支持覆盖上传\n\t\tif y.Addition.FamilyTransfer {\n\t\t\ty.storageConfig.NoOverwriteUpload = true\n\t\t}\n\t}\n\t// 处理个人云和家庭云参数\n\tif y.isFamily() && y.RootFolderID == \"-11\" {\n\t\ty.RootFolderID = \"\"\n\t}\n\tif !y.isFamily() && y.RootFolderID == \"\" {\n\t\ty.RootFolderID = \"-11\"\n\t}\n\n\t// 限制上传线程数\n\ty.uploadThread, _ = strconv.Atoi(y.UploadThread)\n\tif y.uploadThread < 1 || y.uploadThread > 32 {\n\t\ty.uploadThread, y.UploadThread = 3, \"3\"\n\t}\n\n\tif y.ref == nil {\n\t\t// 初始化请求客户端\n\t\tif y.client == nil {\n\t\t\ty.client = base.NewRestyClient().SetHeaders(map[string]string{\n\t\t\t\t\"Accept\":  \"application/json;charset=UTF-8\",\n\t\t\t\t\"Referer\": WEB_URL,\n\t\t\t})\n\t\t}\n\n\t\t// 先尝试用Token刷新，之后尝试登陆\n\t\tif y.Addition.RefreshToken != \"\" {\n\t\t\ty.tokenInfo = &AppSessionResp{RefreshToken: y.Addition.RefreshToken}\n\t\t\tif err = y.refreshToken(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err = y.login(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// 初始化并启动 cron 任务\n\t\ty.cron = cron.NewCron(time.Duration(time.Minute * 5))\n\t\t// 每5分钟执行一次 keepAlive\n\t\ty.cron.Do(y.keepAlive)\n\t}\n\n\t// 处理家庭云ID\n\tif y.FamilyID == \"\" {\n\t\tif y.FamilyID, err = y.getFamilyID(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 创建中转文件夹\n\tif y.FamilyTransfer {\n\t\tif err := y.createFamilyTransferFolder(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 清理转存文件节流\n\ty.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {\n\t\tif err := y.cleanFamilyTransfer(context.TODO()); err != nil {\n\t\t\tutils.Log.Errorf(\"cleanFamilyTransferFolderError:%s\", err)\n\t\t}\n\t})\n\treturn err\n}\n\nfunc (d *Cloud189PC) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*Cloud189PC)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (y *Cloud189PC) Drop(ctx context.Context) error {\n\ty.ref = nil\n\tif y.cron != nil {\n\t\ty.cron.Stop()\n\t\ty.cron = nil\n\t}\n\treturn nil\n}\n\nfunc (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn y.getFiles(ctx, dir.GetID(), y.isFamily())\n}\n\nfunc (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar downloadUrl struct {\n\t\tURL string `json:\"fileDownloadUrl\"`\n\t}\n\n\tisFamily := y.isFamily()\n\tfullUrl := API_URL\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/getFileDownloadUrl.action\"\n\n\t_, err := y.get(fullUrl, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetQueryParam(\"fileId\", file.GetID())\n\t\tif isFamily {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\": y.FamilyID,\n\t\t\t})\n\t\t} else {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"dt\":   \"3\",\n\t\t\t\t\"flag\": \"1\",\n\t\t\t})\n\t\t}\n\t}, &downloadUrl, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 重定向获取真实链接\n\tdownloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, \"&amp;\", \"&\"), \"http://\", \"https://\", 1)\n\tres, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.RawBody().Close()\n\tif res.StatusCode() == 302 {\n\t\tdownloadUrl.URL = res.Header().Get(\"location\")\n\t}\n\n\tlike := &model.Link{\n\t\tURL: downloadUrl.URL,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{base.UserAgent},\n\t\t},\n\t}\n\t/*\n\t\t// 获取链接有效时常\n\t\tstrs := regexp.MustCompile(`(?i)expire[^=]*=([0-9]*)`).FindStringSubmatch(downloadUrl.URL)\n\t\tif len(strs) == 2 {\n\t\t\ttimestamp, err := strconv.ParseInt(strs[1], 10, 64)\n\t\t\tif err == nil {\n\t\t\t\texpired := time.Duration(timestamp-time.Now().Unix()) * time.Second\n\t\t\t\tlike.Expiration = &expired\n\t\t\t}\n\t\t}\n\t*/\n\treturn like, nil\n}\n\nfunc (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tfullUrl := API_URL\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/createFolder.action\"\n\n\tvar newFolder Cloud189Folder\n\t_, err := y.post(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"folderName\":   dirName,\n\t\t\t\"relativePath\": \"\",\n\t\t})\n\t\tif isFamily {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\": y.FamilyID,\n\t\t\t\t\"parentId\": parentDir.GetID(),\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"parentFolderId\": parentDir.GetID(),\n\t\t\t})\n\t\t}\n\t}, &newFolder, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &newFolder, nil\n}\n\nfunc (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tother := map[string]string{\"targetFileName\": dstDir.GetName()}\n\n\tresp, err := y.CreateBatchTask(\"MOVE\", IF(isFamily, y.FamilyID, \"\"), dstDir.GetID(), other, BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = y.WaitBatchTask(\"MOVE\", resp.TaskID, time.Millisecond*400); err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcObj, nil\n}\n\nfunc (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tisFamily := y.isFamily()\n\tqueryParam := make(map[string]string)\n\tfullUrl := API_URL\n\tmethod := http.MethodPost\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t\tmethod = http.MethodGet\n\t\tqueryParam[\"familyId\"] = y.FamilyID\n\t}\n\n\tvar newObj model.Obj\n\tswitch f := srcObj.(type) {\n\tcase *Cloud189File:\n\t\tfullUrl += \"/renameFile.action\"\n\t\tqueryParam[\"fileId\"] = srcObj.GetID()\n\t\tqueryParam[\"destFileName\"] = newName\n\t\tnewObj = &Cloud189File{Icon: f.Icon} // 复用预览\n\tcase *Cloud189Folder:\n\t\tfullUrl += \"/renameFolder.action\"\n\t\tqueryParam[\"folderId\"] = srcObj.GetID()\n\t\tqueryParam[\"destFolderName\"] = newName\n\t\tnewObj = &Cloud189Folder{}\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\n\t_, err := y.request(fullUrl, method, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetQueryParams(queryParam)\n\t}, nil, newObj, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn newObj, nil\n}\n\nfunc (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tisFamily := y.isFamily()\n\tother := map[string]string{\"targetFileName\": dstDir.GetName()}\n\n\tresp, err := y.CreateBatchTask(\"COPY\", IF(isFamily, y.FamilyID, \"\"), dstDir.GetID(), other, BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn y.WaitBatchTask(\"COPY\", resp.TaskID, time.Second)\n}\n\nfunc (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {\n\tisFamily := y.isFamily()\n\n\tresp, err := y.CreateBatchTask(\"DELETE\", IF(isFamily, y.FamilyID, \"\"), \"\", nil, BatchTaskInfo{\n\t\tFileId:   obj.GetID(),\n\t\tFileName: obj.GetName(),\n\t\tIsFolder: BoolToNumber(obj.IsDir()),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 批量任务数量限制，过快会导致无法删除\n\treturn y.WaitBatchTask(\"DELETE\", resp.TaskID, time.Millisecond*200)\n}\n\nfunc (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {\n\toverwrite := true\n\tisFamily := y.isFamily()\n\n\t// 响应时间长,按需启用\n\tif y.Addition.RapidUpload && !stream.IsForceStreamUpload() {\n\t\tif newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {\n\t\t\treturn newObj, nil\n\t\t}\n\t}\n\n\tuploadMethod := y.UploadMethod\n\tif stream.IsForceStreamUpload() {\n\t\tuploadMethod = \"stream\"\n\t}\n\n\t// 旧版上传家庭云也有限制\n\tif uploadMethod == \"old\" {\n\t\treturn y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)\n\t}\n\n\t// 开启家庭云转存\n\tif !isFamily && y.FamilyTransfer {\n\t\t// 修改上传目标为家庭云文件夹\n\t\ttransferDstDir := dstDir\n\t\tdstDir = y.familyTransferFolder\n\n\t\t// 使用临时文件名\n\t\tsrcName := stream.GetName()\n\t\tstream = &WrapFileStreamer{\n\t\t\tFileStreamer: stream,\n\t\t\tName:         fmt.Sprintf(\"0%s.transfer\", uuid.NewString()),\n\t\t}\n\n\t\t// 使用家庭云上传\n\t\tisFamily = true\n\t\toverwrite = false\n\n\t\tdefer func() {\n\t\t\tif newObj != nil {\n\t\t\t\t// 转存家庭云文件到个人云\n\t\t\t\terr = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)\n\t\t\t\t// 删除家庭云源文件\n\t\t\t\tgo y.Delete(context.TODO(), y.FamilyID, newObj)\n\t\t\t\t// 批量任务有概率删不掉\n\t\t\t\tgo y.cleanFamilyTransferFile()\n\t\t\t\t// 转存失败返回错误\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// 查找转存文件\n\t\t\t\tvar file *Cloud189File\n\t\t\t\tfile, err = y.findFileByName(context.TODO(), newObj.GetName(), transferDstDir.GetID(), false)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif err == errs.ObjectNotFound {\n\t\t\t\t\t\terr = fmt.Errorf(\"unknown error: No transfer file obtained %s\", newObj.GetName())\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// 重命名转存文件\n\t\t\t\tnewObj, err = y.Rename(context.TODO(), file, srcName)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// 重命名失败删除源文件\n\t\t\t\t\t_ = y.Delete(context.TODO(), \"\", file)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t}\n\n\tswitch uploadMethod {\n\tcase \"rapid\":\n\t\treturn y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)\n\tcase \"stream\":\n\t\tif stream.GetSize() == 0 {\n\t\t\treturn y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)\n\t\t}\n\t\tfallthrough\n\tdefault:\n\t\treturn y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)\n\t}\n}\n\nfunc (y *Cloud189PC) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tcapacityInfo, err := y.getCapacityInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar total, used int64\n\tif y.isFamily() {\n\t\ttotal = capacityInfo.FamilyCapacityInfo.TotalSize\n\t\tused = capacityInfo.FamilyCapacityInfo.UsedSize\n\t} else {\n\t\ttotal = capacityInfo.CloudCapacityInfo.TotalSize\n\t\tused = capacityInfo.CloudCapacityInfo.UsedSize\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/189pc/help.go",
    "content": "package _189pc\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha1\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n)\n\nfunc clientSuffix() map[string]string {\n\trand := random.Rand\n\treturn map[string]string{\n\t\t\"clientType\": PC,\n\t\t\"version\":    VERSION,\n\t\t\"channelId\":  CHANNEL_ID,\n\t\t\"rand\":       fmt.Sprintf(\"%d_%d\", rand.Int63n(1e5), rand.Int63n(1e10)),\n\t}\n}\n\n// 带params的SignatureOfHmac HMAC签名\nfunc signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, param string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(fullUrl)[1]\n\tmac := hmac.New(sha1.New, []byte(sessionSecret))\n\tdata := fmt.Sprintf(\"SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s\", sessionKey, operate, urlpath, dateOfGmt)\n\tif param != \"\" {\n\t\tdata += fmt.Sprintf(\"&params=%s\", param)\n\t}\n\tmac.Write([]byte(data))\n\treturn strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))\n}\n\n// RAS 加密用户名密码\nfunc RsaEncrypt(publicKey, origData string) string {\n\tblock, _ := pem.Decode([]byte(publicKey))\n\tpubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)\n\tdata, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData))\n\treturn strings.ToUpper(hex.EncodeToString(data))\n}\n\n// aes 加密params\nfunc AesECBEncrypt(data, key string) string {\n\tblock, _ := aes.NewCipher([]byte(key))\n\tpaddingData := PKCS7Padding([]byte(data), block.BlockSize())\n\tdecrypted := make([]byte, len(paddingData))\n\tsize := block.BlockSize()\n\tfor src, dst := paddingData, decrypted; len(src) > 0; src, dst = src[size:], dst[size:] {\n\t\tblock.Encrypt(dst[:size], src[:size])\n\t}\n\treturn strings.ToUpper(hex.EncodeToString(decrypted))\n}\n\nfunc PKCS7Padding(ciphertext []byte, blockSize int) []byte {\n\tpadding := blockSize - len(ciphertext)%blockSize\n\tpadtext := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(ciphertext, padtext...)\n}\n\n// 获取http规范的时间\nfunc getHttpDateStr() string {\n\treturn time.Now().UTC().Format(http.TimeFormat)\n}\n\n// 时间戳\nfunc timestamp() int64 {\n\treturn time.Now().UTC().UnixNano() / 1e6\n}\n\n// formatDate formats a time.Time object into the \"YYYY-MM-DDHH:mm:ssSSS\" format.\nfunc formatDate(t time.Time) string {\n\t// The layout string \"2006-01-0215:04:05.000\" corresponds to:\n\t// 2006 -> Year (YYYY)\n\t// 01   -> Month (MM)\n\t// 02   -> Day (DD)\n\t// 15   -> Hour (HH)\n\t// 04   -> Minute (mm)\n\t// 05   -> Second (ss)\n\t// 000  -> Millisecond (SSS) with leading zeros\n\t// Note the lack of a separator between the date and hour, matching the desired output.\n\treturn t.Format(\"2006-01-0215:04:05.000\")\n}\n\nfunc MustParseTime(str string) *time.Time {\n\tlastOpTime, _ := time.ParseInLocation(\"2006-01-02 15:04:05 -07\", str+\" +08\", time.Local)\n\treturn &lastOpTime\n}\n\ntype Time time.Time\n\nfunc (t *Time) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }\nfunc (t *Time) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {\n\tb, err := e.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif b, ok := b.(xml.CharData); ok {\n\t\tif err = t.Unmarshal(b); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn e.Skip()\n}\nfunc (t *Time) Unmarshal(b []byte) error {\n\tbs := strings.Trim(string(b), \"\\\"\")\n\tvar v time.Time\n\tvar err error\n\tfor _, f := range []string{\"2006-01-02 15:04:05 -07\", \"Jan 2, 2006 15:04:05 PM -07\"} {\n\t\tv, err = time.ParseInLocation(f, bs+\" +08\", time.Local)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\t*t = Time(v)\n\treturn err\n}\n\ntype String string\n\nfunc (t *String) UnmarshalJSON(b []byte) error { return t.Unmarshal(b) }\nfunc (t *String) UnmarshalXML(e *xml.Decoder, ee xml.StartElement) error {\n\tb, err := e.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif b, ok := b.(xml.CharData); ok {\n\t\tif err = t.Unmarshal(b); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn e.Skip()\n}\nfunc (s *String) Unmarshal(b []byte) error {\n\t*s = String(bytes.Trim(b, \"\\\"\"))\n\treturn nil\n}\n\nfunc toFamilyOrderBy(o string) string {\n\tswitch o {\n\tcase \"filename\":\n\t\treturn \"1\"\n\tcase \"filesize\":\n\t\treturn \"2\"\n\tcase \"lastOpTime\":\n\t\treturn \"3\"\n\tdefault:\n\t\treturn \"1\"\n\t}\n}\n\nfunc toDesc(o string) string {\n\tswitch o {\n\tcase \"desc\":\n\t\treturn \"true\"\n\tcase \"asc\":\n\t\tfallthrough\n\tdefault:\n\t\treturn \"false\"\n\t}\n}\n\nfunc ParseHttpHeader(str string) map[string]string {\n\theader := make(map[string]string)\n\tfor _, value := range strings.Split(str, \"&\") {\n\t\tif k, v, found := strings.Cut(value, \"=\"); found {\n\t\t\theader[k] = v\n\t\t}\n\t}\n\treturn header\n}\n\nfunc MustString(str string, err error) string {\n\treturn str\n}\n\nfunc BoolToNumber(b bool) int {\n\tif b {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// 计算分片大小\n// 对分片数量有限制\n// 10MIB 20 MIB 999片\n// 50MIB 60MIB 70MIB 80MIB ∞MIB 1999片\nfunc partSize(size int64) int64 {\n\tconst DEFAULT = 1024 * 1024 * 10 // 10MIB\n\tif size > DEFAULT*2*999 {\n\t\treturn int64(math.Max(math.Ceil((float64(size)/1999) /*=单个切片大小*/ /float64(DEFAULT)) /*=倍率*/, 5) * DEFAULT)\n\t}\n\tif size > DEFAULT*999 {\n\t\treturn DEFAULT * 2 // 20MIB\n\t}\n\treturn DEFAULT\n}\n\nfunc isBool(bs ...bool) bool {\n\tfor _, b := range bs {\n\t\tif b {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc IF[V any](o bool, t V, f V) V {\n\tif o {\n\t\treturn t\n\t}\n\treturn f\n}\n\ntype WrapFileStreamer struct {\n\tmodel.FileStreamer\n\tName string\n}\n\nfunc (w *WrapFileStreamer) GetName() string {\n\treturn w.Name\n}\n"
  },
  {
    "path": "drivers/189pc/meta.go",
    "content": "package _189pc\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tLoginType    string `json:\"login_type\" type:\"select\" options:\"password,qrcode\" default:\"password\" required:\"true\"`\n\tUsername     string `json:\"username\" required:\"true\"`\n\tPassword     string `json:\"password\" required:\"true\"`\n\tVCode        string `json:\"validate_code\"`\n\tRefreshToken string `json:\"refresh_token\" help:\"To switch accounts, please clear this field\"`\n\tdriver.RootID\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"filename,filesize,lastOpTime\" default:\"filename\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tType           string `json:\"type\" type:\"select\" options:\"personal,family\" default:\"personal\"`\n\tFamilyID       string `json:\"family_id\"`\n\tUploadMethod   string `json:\"upload_method\" type:\"select\" options:\"stream,rapid,old\" default:\"stream\"`\n\tUploadThread   string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\n\tFamilyTransfer bool   `json:\"family_transfer\"`\n\tRapidUpload    bool   `json:\"rapid_upload\"`\n\tNoUseOcr       bool   `json:\"no_use_ocr\"`\n}\n\nvar config = driver.Config{\n\tName:        \"189CloudPC\",\n\tDefaultRoot: \"-11\",\n\tCheckStatus: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Cloud189PC{}\n\t})\n}\n"
  },
  {
    "path": "drivers/189pc/types.go",
    "content": "package _189pc\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// 居然有四种返回方式\ntype RespErr struct {\n\tResCode    any    `json:\"res_code\"` // int or string\n\tResMessage string `json:\"res_message\"`\n\n\tError_ string `json:\"error\"`\n\n\tXMLName xml.Name `xml:\"error\"`\n\tCode    string   `json:\"code\" xml:\"code\"`\n\tMessage string   `json:\"message\" xml:\"message\"`\n\tMsg     string   `json:\"msg\"`\n\n\tErrorCode string `json:\"errorCode\"`\n\tErrorMsg  string `json:\"errorMsg\"`\n}\n\nfunc (e *RespErr) HasError() bool {\n\tswitch v := e.ResCode.(type) {\n\tcase int, int64, int32:\n\t\treturn v != 0\n\tcase string:\n\t\treturn e.ResCode != \"\"\n\t}\n\treturn (e.Code != \"\" && e.Code != \"SUCCESS\") || e.ErrorCode != \"\" || e.Error_ != \"\"\n}\n\nfunc (e *RespErr) Error() string {\n\tswitch v := e.ResCode.(type) {\n\tcase int, int64, int32:\n\t\tif v != 0 {\n\t\t\treturn fmt.Sprintf(\"res_code: %d ,res_msg: %s\", v, e.ResMessage)\n\t\t}\n\tcase string:\n\t\tif e.ResCode != \"\" {\n\t\t\treturn fmt.Sprintf(\"res_code: %s ,res_msg: %s\", e.ResCode, e.ResMessage)\n\t\t}\n\t}\n\n\tif e.Code != \"\" && e.Code != \"SUCCESS\" {\n\t\tif e.Msg != \"\" {\n\t\t\treturn fmt.Sprintf(\"code: %s ,msg: %s\", e.Code, e.Msg)\n\t\t}\n\t\tif e.Message != \"\" {\n\t\t\treturn fmt.Sprintf(\"code: %s ,msg: %s\", e.Code, e.Message)\n\t\t}\n\t\treturn \"code: \" + e.Code\n\t}\n\n\tif e.ErrorCode != \"\" {\n\t\treturn fmt.Sprintf(\"err_code: %s ,err_msg: %s\", e.ErrorCode, e.ErrorMsg)\n\t}\n\n\tif e.Error_ != \"\" {\n\t\treturn fmt.Sprintf(\"error: %s ,message: %s\", e.ErrorCode, e.Message)\n\t}\n\treturn \"\"\n}\n\ntype BaseLoginParam struct {\n\t// 请求头参数\n\tLt    string\n\tReqId string\n\n\t// 表单参数\n\tParamId string\n\n\t// 验证码\n\tCaptchaToken string\n}\n\n// QRLoginParam 用于暂存二维码登录过程中的参数\ntype QRLoginParam struct {\n\tBaseLoginParam\n\n\tUUID       string `json:\"uuid\"`\n\tEncodeUUID string `json:\"encodeuuid\"`\n\tEncryUUID  string `json:\"encryuuid\"`\n}\n\n// 登陆需要的参数\ntype LoginParam struct {\n\t// 加密后的用户名和密码\n\tRsaUsername string\n\tRsaPassword string\n\n\t// rsa密钥\n\tjRsaKey string\n\n\tBaseLoginParam\n}\n\n// 登陆加密相关\ntype EncryptConfResp struct {\n\tResult int `json:\"result\"`\n\tData   struct {\n\t\tUpSmsOn   string `json:\"upSmsOn\"`\n\t\tPre       string `json:\"pre\"`\n\t\tPreDomain string `json:\"preDomain\"`\n\t\tPubKey    string `json:\"pubKey\"`\n\t} `json:\"data\"`\n}\n\ntype LoginResp struct {\n\tMsg    string `json:\"msg\"`\n\tResult int    `json:\"result\"`\n\tToUrl  string `json:\"toUrl\"`\n}\n\n// 刷新session返回\ntype UserSessionResp struct {\n\tResCode    int    `json:\"res_code\"`\n\tResMessage string `json:\"res_message\"`\n\n\tLoginName string `json:\"loginName\"`\n\n\tKeepAlive       int `json:\"keepAlive\"`\n\tGetFileDiffSpan int `json:\"getFileDiffSpan\"`\n\tGetUserInfoSpan int `json:\"getUserInfoSpan\"`\n\n\t// 个人云\n\tSessionKey    string `json:\"sessionKey\"`\n\tSessionSecret string `json:\"sessionSecret\"`\n\t// 家庭云\n\tFamilySessionKey    string `json:\"familySessionKey\"`\n\tFamilySessionSecret string `json:\"familySessionSecret\"`\n}\n\n// 登录返回\ntype AppSessionResp struct {\n\tUserSessionResp\n\n\tIsSaveName string `json:\"isSaveName\"`\n\n\t// 会话刷新Token\n\tAccessToken string `json:\"accessToken\"`\n\t//Token刷新\n\tRefreshToken string `json:\"refreshToken\"`\n}\n\n// 家庭云账户\ntype FamilyInfoListResp struct {\n\tFamilyInfoResp []FamilyInfoResp `json:\"familyInfoResp\"`\n}\ntype FamilyInfoResp struct {\n\tCount      int    `json:\"count\"`\n\tCreateTime string `json:\"createTime\"`\n\tFamilyID   int64  `json:\"familyId\"`\n\tRemarkName string `json:\"remarkName\"`\n\tType       int    `json:\"type\"`\n\tUseFlag    int    `json:\"useFlag\"`\n\tUserRole   int    `json:\"userRole\"`\n}\n\n/*文件部分*/\n// 文件\ntype Cloud189File struct {\n\tID   String `json:\"id\"`\n\tName string `json:\"name\"`\n\tSize int64  `json:\"size\"`\n\tMd5  string `json:\"md5\"`\n\n\tLastOpTime Time `json:\"lastOpTime\"`\n\tCreateDate Time `json:\"createDate\"`\n\tIcon       struct {\n\t\t//iconOption 5\n\t\tSmallUrl string `json:\"smallUrl\"`\n\t\tLargeUrl string `json:\"largeUrl\"`\n\n\t\t// iconOption 10\n\t\tMax600    string `json:\"max600\"`\n\t\tMediumURL string `json:\"mediumUrl\"`\n\t} `json:\"icon\"`\n\n\t// Orientation int64  `json:\"orientation\"`\n\t// FileCata   int64  `json:\"fileCata\"`\n\t// MediaType   int    `json:\"mediaType\"`\n\t// Rev         string `json:\"rev\"`\n\t// StarLabel   int64  `json:\"starLabel\"`\n}\n\nfunc (c *Cloud189File) CreateTime() time.Time {\n\treturn time.Time(c.CreateDate)\n}\n\nfunc (c *Cloud189File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.MD5, c.Md5)\n}\n\nfunc (c *Cloud189File) GetSize() int64     { return c.Size }\nfunc (c *Cloud189File) GetName() string    { return c.Name }\nfunc (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }\nfunc (c *Cloud189File) IsDir() bool        { return false }\nfunc (c *Cloud189File) GetID() string      { return string(c.ID) }\nfunc (c *Cloud189File) GetPath() string    { return \"\" }\nfunc (c *Cloud189File) Thumb() string      { return c.Icon.SmallUrl }\n\n// 文件夹\ntype Cloud189Folder struct {\n\tID       String `json:\"id\"`\n\tParentID int64  `json:\"parentId\"`\n\tName     string `json:\"name\"`\n\n\tLastOpTime Time `json:\"lastOpTime\"`\n\tCreateDate Time `json:\"createDate\"`\n\n\t// FileListSize int64 `json:\"fileListSize\"`\n\t// FileCount int64 `json:\"fileCount\"`\n\t// FileCata  int64 `json:\"fileCata\"`\n\t// Rev          string `json:\"rev\"`\n\t// StarLabel    int64  `json:\"starLabel\"`\n}\n\nfunc (c *Cloud189Folder) CreateTime() time.Time {\n\treturn time.Time(c.CreateDate)\n}\n\nfunc (c *Cloud189Folder) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (c *Cloud189Folder) GetSize() int64     { return 0 }\nfunc (c *Cloud189Folder) GetName() string    { return c.Name }\nfunc (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }\nfunc (c *Cloud189Folder) IsDir() bool        { return true }\nfunc (c *Cloud189Folder) GetID() string      { return string(c.ID) }\nfunc (c *Cloud189Folder) GetPath() string    { return \"\" }\n\ntype Cloud189FilesResp struct {\n\t//ResCode    int    `json:\"res_code\"`\n\t//ResMessage string `json:\"res_message\"`\n\tFileListAO struct {\n\t\tCount      int              `json:\"count\"`\n\t\tFileList   []Cloud189File   `json:\"fileList\"`\n\t\tFolderList []Cloud189Folder `json:\"folderList\"`\n\t} `json:\"fileListAO\"`\n}\n\n// TaskInfo 任务信息\ntype BatchTaskInfo struct {\n\t// FileId 文件ID\n\tFileId string `json:\"fileId\"`\n\t// FileName 文件名\n\tFileName string `json:\"fileName\"`\n\t// IsFolder 是否是文件夹，0-否，1-是\n\tIsFolder int `json:\"isFolder\"`\n\t// SrcParentId 文件所在父目录ID\n\tSrcParentId string `json:\"srcParentId,omitempty\"`\n\n\t/* 冲突管理 */\n\t// 1 -> 跳过 2 -> 保留 3 -> 覆盖\n\tDealWay    int `json:\"dealWay,omitempty\"`\n\tIsConflict int `json:\"isConflict,omitempty\"`\n}\n\n/* 上传部分 */\ntype InitMultiUploadResp struct {\n\t//Code string `json:\"code\"`\n\tData struct {\n\t\tUploadType     int    `json:\"uploadType\"`\n\t\tUploadHost     string `json:\"uploadHost\"`\n\t\tUploadFileID   string `json:\"uploadFileId\"`\n\t\tFileDataExists int    `json:\"fileDataExists\"`\n\t} `json:\"data\"`\n}\ntype UploadUrlsResp struct {\n\tCode string                    `json:\"code\"`\n\tData map[string]UploadUrlsData `json:\"uploadUrls\"`\n}\ntype UploadUrlsData struct {\n\tRequestURL    string `json:\"requestURL\"`\n\tRequestHeader string `json:\"requestHeader\"`\n}\n\ntype UploadUrlInfo struct {\n\tPartNumber int\n\tHeaders    map[string]string\n\tUploadUrlsData\n}\n\ntype UploadProgress struct {\n\tUploadInfo  InitMultiUploadResp\n\tUploadParts []string\n}\n\n/* 第二种上传方式 */\ntype CreateUploadFileResp struct {\n\t// 上传文件请求ID\n\tUploadFileId int64 `json:\"uploadFileId\"`\n\t// 上传文件数据的URL路径\n\tFileUploadUrl string `json:\"fileUploadUrl\"`\n\t// 上传文件完成后确认路径\n\tFileCommitUrl string `json:\"fileCommitUrl\"`\n\t// 文件是否已存在云盘中，0-未存在，1-已存在\n\tFileDataExists int `json:\"fileDataExists\"`\n}\n\ntype GetUploadFileStatusResp struct {\n\tCreateUploadFileResp\n\n\t// 已上传的大小\n\tDataSize int64 `json:\"dataSize\"`\n\tSize     int64 `json:\"size\"`\n}\n\nfunc (r *GetUploadFileStatusResp) GetSize() int64 {\n\treturn r.DataSize + r.Size\n}\n\ntype CommitMultiUploadFileResp struct {\n\tFile struct {\n\t\tUserFileID String `json:\"userFileId\"`\n\t\tFileName   string `json:\"fileName\"`\n\t\tFileSize   int64  `json:\"fileSize\"`\n\t\tFileMd5    string `json:\"fileMd5\"`\n\t\tCreateDate Time   `json:\"createDate\"`\n\t} `json:\"file\"`\n}\n\nfunc (f *CommitMultiUploadFileResp) toFile() *Cloud189File {\n\treturn &Cloud189File{\n\t\tID:         f.File.UserFileID,\n\t\tName:       f.File.FileName,\n\t\tSize:       f.File.FileSize,\n\t\tMd5:        f.File.FileMd5,\n\t\tLastOpTime: f.File.CreateDate,\n\t\tCreateDate: f.File.CreateDate,\n\t}\n}\n\ntype OldCommitUploadFileResp struct {\n\tXMLName    xml.Name `xml:\"file\"`\n\tID         String   `xml:\"id\"`\n\tName       string   `xml:\"name\"`\n\tSize       int64    `xml:\"size\"`\n\tMd5        string   `xml:\"md5\"`\n\tCreateDate Time     `xml:\"createDate\"`\n}\n\nfunc (f *OldCommitUploadFileResp) toFile() *Cloud189File {\n\treturn &Cloud189File{\n\t\tID:         f.ID,\n\t\tName:       f.Name,\n\t\tSize:       f.Size,\n\t\tMd5:        f.Md5,\n\t\tCreateDate: f.CreateDate,\n\t\tLastOpTime: f.CreateDate,\n\t}\n}\n\ntype CreateBatchTaskResp struct {\n\tTaskID string `json:\"taskId\"`\n}\n\ntype BatchTaskStateResp struct {\n\tFailedCount         int     `json:\"failedCount\"`\n\tProcess             int     `json:\"process\"`\n\tSkipCount           int     `json:\"skipCount\"`\n\tSubTaskCount        int     `json:\"subTaskCount\"`\n\tSuccessedCount      int     `json:\"successedCount\"`\n\tSuccessedFileIDList []int64 `json:\"successedFileIdList\"`\n\tTaskID              string  `json:\"taskId\"`\n\tTaskStatus          int     `json:\"taskStatus\"` //1 初始化 2 存在冲突 3 执行中，4 完成\n}\n\ntype BatchTaskConflictTaskInfoResp struct {\n\tSessionKey     string `json:\"sessionKey\"`\n\tTargetFolderID int    `json:\"targetFolderId\"`\n\tTaskID         string `json:\"taskId\"`\n\tTaskInfos      []BatchTaskInfo\n\tTaskType       int `json:\"taskType\"`\n}\n\n/* query 加密参数*/\ntype Params map[string]string\n\nfunc (p Params) Set(k, v string) {\n\tp[k] = v\n}\n\nfunc (p Params) Encode() string {\n\tif p == nil {\n\t\treturn \"\"\n\t}\n\tvar buf strings.Builder\n\tkeys := make([]string, 0, len(p))\n\tfor k := range p {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\tfor i := range keys {\n\t\tif buf.Len() > 0 {\n\t\t\tbuf.WriteByte('&')\n\t\t}\n\t\tbuf.WriteString(keys[i])\n\t\tbuf.WriteByte('=')\n\t\tbuf.WriteString(p[keys[i]])\n\t}\n\treturn buf.String()\n}\n\ntype CapacityResp struct {\n\tResCode           int    `json:\"res_code\"`\n\tResMessage        string `json:\"res_message\"`\n\tAccount           string `json:\"account\"`\n\tCloudCapacityInfo struct {\n\t\tFreeSize     int64 `json:\"freeSize\"`\n\t\tMailUsedSize int64 `json:\"mail189UsedSize\"`\n\t\tTotalSize    int64 `json:\"totalSize\"`\n\t\tUsedSize     int64 `json:\"usedSize\"`\n\t} `json:\"cloudCapacityInfo\"`\n\tFamilyCapacityInfo struct {\n\t\tFreeSize  int64 `json:\"freeSize\"`\n\t\tTotalSize int64 `json:\"totalSize\"`\n\t\tUsedSize  int64 `json:\"usedSize\"`\n\t} `json:\"familyCapacityInfo\"`\n\tTotalSize uint64 `json:\"totalSize\"`\n}\n"
  },
  {
    "path": "drivers/189pc/utils.go",
    "content": "package _189pc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/skip2/go-qrcode\"\n\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/pkg/errors\"\n)\n\nconst (\n\tACCOUNT_TYPE = \"02\"\n\tAPP_ID       = \"8025431004\"\n\tCLIENT_TYPE  = \"10020\"\n\tVERSION      = \"6.2\"\n\n\tWEB_URL    = \"https://cloud.189.cn\"\n\tAUTH_URL   = \"https://open.e.189.cn\"\n\tAPI_URL    = \"https://api.cloud.189.cn\"\n\tUPLOAD_URL = \"https://upload.cloud.189.cn\"\n\n\tRETURN_URL = \"https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html\"\n\n\tPC  = \"TELEPC\"\n\tMAC = \"TELEMAC\"\n\n\tCHANNEL_ID = \"web_cloud.189.cn\"\n\n\t// Error codes\n\tUserInvalidOpenTokenError = \"UserInvalidOpenToken\"\n)\n\nfunc (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {\n\tdateOfGmt := getHttpDateStr()\n\tsessionKey := y.getTokenInfo().SessionKey\n\tsessionSecret := y.getTokenInfo().SessionSecret\n\tif isFamily {\n\t\tsessionKey = y.getTokenInfo().FamilySessionKey\n\t\tsessionSecret = y.getTokenInfo().FamilySessionSecret\n\t}\n\n\theader := map[string]string{\n\t\t\"Date\":         dateOfGmt,\n\t\t\"SessionKey\":   sessionKey,\n\t\t\"X-Request-ID\": uuid.NewString(),\n\t\t\"Signature\":    signatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt, params),\n\t}\n\treturn header\n}\n\nfunc (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {\n\tsessionSecret := y.getTokenInfo().SessionSecret\n\tif isFamily {\n\t\tsessionSecret = y.getTokenInfo().FamilySessionSecret\n\t}\n\tif params != nil {\n\t\treturn AesECBEncrypt(params.Encode(), sessionSecret[:16])\n\t}\n\treturn \"\"\n}\n\nfunc (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {\n\tif y.getTokenInfo() == nil {\n\t\treturn nil, fmt.Errorf(\"login failed\")\n\t}\n\treq := y.getClient().R().SetQueryParams(clientSuffix())\n\n\t// 设置params\n\tparamsData := y.EncryptParams(params, isBool(isFamily...))\n\tif paramsData != \"\" {\n\t\treq.SetQueryParam(\"params\", paramsData)\n\t}\n\n\t// Signature\n\treq.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))\n\n\tvar erron RespErr\n\treq.SetError(&erron)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif strings.Contains(res.String(), \"userSessionBO is null\") {\n\t\tif err = y.refreshSession(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn y.request(url, method, callback, params, resp, isFamily...)\n\t}\n\n\t// if erron.ErrorCode == \"InvalidSessionKey\" || erron.Code == \"InvalidSessionKey\" {\n\tif strings.Contains(res.String(), \"InvalidSessionKey\") {\n\t\tif err = y.refreshSession(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn y.request(url, method, callback, params, resp, isFamily...)\n\t}\n\n\t// 处理错误\n\tif erron.HasError() {\n\t\treturn nil, &erron\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {\n\treturn y.request(url, http.MethodGet, callback, nil, resp, isFamily...)\n}\n\nfunc (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {\n\treturn y.request(url, http.MethodPost, callback, nil, resp, isFamily...)\n}\n\nfunc (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := req.URL.Query()\n\tfor key, value := range clientSuffix() {\n\t\tquery.Add(key, value)\n\t}\n\treq.URL.RawQuery = query.Encode()\n\n\tfor key, value := range headers {\n\t\treq.Header.Add(key, value)\n\t}\n\n\tif sign {\n\t\tfor key, value := range y.SignatureHeader(url, http.MethodPut, \"\", isFamily) {\n\t\t\treq.Header.Add(key, value)\n\t\t}\n\t}\n\n\tresp, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar erron RespErr\n\t_ = jsoniter.Unmarshal(body, &erron)\n\t_ = xml.Unmarshal(body, &erron)\n\tif erron.HasError() {\n\t\treturn nil, &erron\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Errorf(\"put fail,err:%s\", string(body))\n\t}\n\treturn body, nil\n}\n\nfunc (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {\n\tres := make([]model.Obj, 0, 100)\n\tfor pageNum := 1; ; pageNum++ {\n\t\tresp, err := y.getFilesWithPage(ctx, fileId, isFamily, pageNum, 1000, y.OrderBy, y.OrderDirection)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 获取完毕跳出\n\t\tif resp.FileListAO.Count == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tfor i := 0; i < len(resp.FileListAO.FolderList); i++ {\n\t\t\tres = append(res, &resp.FileListAO.FolderList[i])\n\t\t}\n\t\tfor i := 0; i < len(resp.FileListAO.FileList); i++ {\n\t\t\tres = append(res, &resp.FileListAO.FileList[i])\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (y *Cloud189PC) getFilesWithPage(ctx context.Context, fileId string, isFamily bool, pageNum int, pageSize int, orderBy string, orderDirection string) (*Cloud189FilesResp, error) {\n\tfullUrl := API_URL\n\tif isFamily {\n\t\tfullUrl += \"/family/file\"\n\t}\n\tfullUrl += \"/listFiles.action\"\n\n\tvar resp Cloud189FilesResp\n\t_, err := y.get(fullUrl, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetQueryParams(map[string]string{\n\t\t\t\"folderId\":   fileId,\n\t\t\t\"fileType\":   \"0\",\n\t\t\t\"mediaAttr\":  \"0\",\n\t\t\t\"iconOption\": \"5\",\n\t\t\t\"pageNum\":    fmt.Sprint(pageNum),\n\t\t\t\"pageSize\":   fmt.Sprint(pageSize),\n\t\t})\n\t\tif isFamily {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\":   y.FamilyID,\n\t\t\t\t\"orderBy\":    toFamilyOrderBy(orderBy),\n\t\t\t\t\"descending\": toDesc(orderDirection),\n\t\t\t})\n\t\t} else {\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"recursive\":  \"0\",\n\t\t\t\t\"orderBy\":    orderBy,\n\t\t\t\t\"descending\": toDesc(orderDirection),\n\t\t\t})\n\t\t}\n\t}, &resp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, folderId string, isFamily bool) (*Cloud189File, error) {\n\tfor pageNum := 1; ; pageNum++ {\n\t\tresp, err := y.getFilesWithPage(ctx, folderId, isFamily, pageNum, 10, \"filename\", \"asc\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 获取完毕跳出\n\t\tif resp.FileListAO.Count == 0 {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t\tfor i := 0; i < len(resp.FileListAO.FileList); i++ {\n\t\t\tfile := resp.FileListAO.FileList[i]\n\t\t\tif file.Name == searchName {\n\t\t\t\treturn &file, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (y *Cloud189PC) login() error {\n\tif y.LoginType == \"qrcode\" {\n\t\treturn y.loginByQRCode()\n\t}\n\treturn y.loginByPassword()\n}\n\nfunc (y *Cloud189PC) loginByPassword() (err error) {\n\t// 初始化登陆所需参数\n\tif y.loginParam == nil {\n\t\tif err = y.initLoginParam(); err != nil {\n\t\t\t// 验证码也通过错误返回\n\t\t\treturn err\n\t\t}\n\t}\n\tdefer func() {\n\t\t// 销毁验证码\n\t\ty.VCode = \"\"\n\t\t// 销毁登陆参数\n\t\ty.loginParam = nil\n\t\t// 遇到错误，重新加载登陆参数(刷新验证码)\n\t\tif err != nil {\n\t\t\tif y.NoUseOcr {\n\t\t\t\tif err1 := y.initLoginParam(); err1 != nil {\n\t\t\t\t\terr = fmt.Errorf(\"err1: %s \\nerr2: %s\", err, err1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ty.Status = err.Error()\n\t\t\top.MustSaveDriverStorage(y)\n\t\t}\n\t}()\n\n\tparam := y.loginParam\n\tvar loginresp LoginResp\n\t_, err = y.client.R().\n\t\tForceContentType(\"application/json;charset=UTF-8\").SetResult(&loginresp).\n\t\tSetHeaders(map[string]string{\n\t\t\t\"REQID\": param.ReqId,\n\t\t\t\"lt\":    param.Lt,\n\t\t}).\n\t\tSetFormData(map[string]string{\n\t\t\t\"appKey\":       APP_ID,\n\t\t\t\"accountType\":  ACCOUNT_TYPE,\n\t\t\t\"userName\":     param.RsaUsername,\n\t\t\t\"password\":     param.RsaPassword,\n\t\t\t\"validateCode\": y.VCode,\n\t\t\t\"captchaToken\": param.CaptchaToken,\n\t\t\t\"returnUrl\":    RETURN_URL,\n\t\t\t// \"mailSuffix\":   \"@189.cn\",\n\t\t\t\"dynamicCheck\": \"FALSE\",\n\t\t\t\"clientType\":   CLIENT_TYPE,\n\t\t\t\"cb_SaveName\":  \"1\",\n\t\t\t\"isOauth2\":     \"false\",\n\t\t\t\"state\":        \"\",\n\t\t\t\"paramId\":      param.ParamId,\n\t\t}).\n\t\tPost(AUTH_URL + \"/api/logbox/oauth2/loginSubmit.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif loginresp.ToUrl == \"\" {\n\t\treturn fmt.Errorf(\"login failed,No toUrl obtained, msg: %s\", loginresp.Msg)\n\t}\n\n\t// 获取Session\n\tvar erron RespErr\n\tvar tokenInfo AppSessionResp\n\t_, err = y.client.R().\n\t\tSetResult(&tokenInfo).SetError(&erron).\n\t\tSetQueryParams(clientSuffix()).\n\t\tSetQueryParam(\"redirectURL\", loginresp.ToUrl).\n\t\tPost(API_URL + \"/getSessionForPC.action\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif erron.HasError() {\n\t\treturn &erron\n\t}\n\tif tokenInfo.ResCode != 0 {\n\t\terr = fmt.Errorf(tokenInfo.ResMessage)\n\t\treturn err\n\t}\n\ty.Addition.RefreshToken = tokenInfo.RefreshToken\n\ty.tokenInfo = &tokenInfo\n\top.MustSaveDriverStorage(y)\n\treturn err\n}\n\nfunc (y *Cloud189PC) loginByQRCode() error {\n\tif y.qrcodeParam == nil {\n\t\tif err := y.initQRCodeParam(); err != nil {\n\t\t\t// 二维码也通过错误返回\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar state struct {\n\t\tStatus      int    `json:\"status\"`\n\t\tRedirectUrl string `json:\"redirectUrl\"`\n\t\tMsg         string `json:\"msg\"`\n\t}\n\n\tnow := time.Now()\n\t_, err := y.client.R().\n\t\tSetHeaders(map[string]string{\n\t\t\t\"Referer\": AUTH_URL,\n\t\t\t\"Reqid\":   y.qrcodeParam.ReqId,\n\t\t\t\"lt\":      y.qrcodeParam.Lt,\n\t\t}).\n\t\tSetFormData(map[string]string{\n\t\t\t\"appId\":      APP_ID,\n\t\t\t\"clientType\": CLIENT_TYPE,\n\t\t\t\"returnUrl\":  RETURN_URL,\n\t\t\t\"paramId\":    y.qrcodeParam.ParamId,\n\t\t\t\"uuid\":       y.qrcodeParam.UUID,\n\t\t\t\"encryuuid\":  y.qrcodeParam.EncryUUID,\n\t\t\t\"date\":       formatDate(now),\n\t\t\t\"timeStamp\":  fmt.Sprint(now.UTC().UnixNano() / 1e6),\n\t\t}).\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetResult(&state).\n\t\tPost(AUTH_URL + \"/api/logbox/oauth2/qrcodeLoginState.do\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to check QR code state: %w\", err)\n\t}\n\n\tswitch state.Status {\n\tcase 0: // 登录成功\n\t\tvar tokenInfo AppSessionResp\n\t\t_, err = y.client.R().\n\t\t\tSetResult(&tokenInfo).\n\t\t\tSetQueryParams(clientSuffix()).\n\t\t\tSetQueryParam(\"redirectURL\", state.RedirectUrl).\n\t\t\tPost(API_URL + \"/getSessionForPC.action\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif tokenInfo.ResCode != 0 {\n\t\t\treturn fmt.Errorf(tokenInfo.ResMessage)\n\t\t}\n\t\ty.Addition.RefreshToken = tokenInfo.RefreshToken\n\t\ty.tokenInfo = &tokenInfo\n\t\top.MustSaveDriverStorage(y)\n\t\treturn nil\n\tcase -11001: // 二维码过期\n\t\ty.qrcodeParam = nil\n\t\treturn errors.New(\"QR code expired, please try again\")\n\tcase -106: // 等待扫描\n\t\treturn y.genQRCode(\"QR code has not been scanned yet, please scan and save again\")\n\tcase -11002: // 等待确认\n\t\treturn y.genQRCode(\"QR code has been scanned, please confirm the login on your phone and save again\")\n\tdefault: // 其他错误\n\t\ty.qrcodeParam = nil\n\t\treturn fmt.Errorf(\"QR code login failed with status %d: %s\", state.Status, state.Msg)\n\t}\n}\n\nfunc (y *Cloud189PC) genQRCode(text string) error {\n\t// 展示二维码\n\tqrTemplate := `<body>\n\tstate: %s\n\t<br><img src=\"data:image/jpeg;base64,%s\"/>\n    <br>Or Click here: <a href=\"%s\">Login</a>\n</body>`\n\n\t// Generate QR code\n\tqrCode, err := qrcode.Encode(y.qrcodeParam.UUID, qrcode.Medium, 256)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate QR code: %v\", err)\n\t}\n\n\t// Encode QR code to base64\n\tqrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)\n\n\t// Create the HTML page\n\tqrPage := fmt.Sprintf(qrTemplate, text, qrCodeBase64, y.qrcodeParam.UUID)\n\treturn fmt.Errorf(\"need verify: \\n%s\", qrPage)\n}\n\nfunc (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {\n\t// 清除cookie\n\tjar, _ := cookiejar.New(nil)\n\ty.client.SetCookieJar(jar)\n\n\tres, err := y.client.R().\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"appId\":      APP_ID,\n\t\t\t\"clientType\": CLIENT_TYPE,\n\t\t\t\"returnURL\":  RETURN_URL,\n\t\t\t\"timeStamp\":  fmt.Sprint(timestamp()),\n\t\t}).\n\t\tGet(WEB_URL + \"/api/portal/unifyLoginForPC.action\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &BaseLoginParam{\n\t\tCaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],\n\t\tLt:           regexp.MustCompile(`lt = \"(.+?)\"`).FindStringSubmatch(res.String())[1],\n\t\tParamId:      regexp.MustCompile(`paramId = \"(.+?)\"`).FindStringSubmatch(res.String())[1],\n\t\tReqId:        regexp.MustCompile(`reqId = \"(.+?)\"`).FindStringSubmatch(res.String())[1],\n\t}, nil\n}\n\n/* 初始化登陆需要的参数\n *  如果遇到验证码返回错误\n */\nfunc (y *Cloud189PC) initLoginParam() error {\n\ty.loginParam = nil\n\n\tbaseParam, err := y.initBaseParams()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ty.loginParam = &LoginParam{BaseLoginParam: *baseParam}\n\n\t// 获取rsa公钥\n\tvar encryptConf EncryptConfResp\n\t_, err = y.client.R().\n\t\tForceContentType(\"application/json;charset=UTF-8\").SetResult(&encryptConf).\n\t\tSetFormData(map[string]string{\"appId\": APP_ID}).\n\t\tPost(AUTH_URL + \"/api/logbox/config/encryptConf.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ty.loginParam.jRsaKey = fmt.Sprintf(\"-----BEGIN PUBLIC KEY-----\\n%s\\n-----END PUBLIC KEY-----\", encryptConf.Data.PubKey)\n\ty.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username)\n\ty.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password)\n\n\t// 判断是否需要验证码\n\tresp, err := y.client.R().\n\t\tSetHeader(\"REQID\", y.loginParam.ReqId).\n\t\tSetFormData(map[string]string{\n\t\t\t\"appKey\":      APP_ID,\n\t\t\t\"accountType\": ACCOUNT_TYPE,\n\t\t\t\"userName\":    y.loginParam.RsaUsername,\n\t\t}).Post(AUTH_URL + \"/api/logbox/oauth2/needcaptcha.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.String() == \"0\" {\n\t\treturn nil\n\t}\n\n\t// 拉取验证码\n\timgRes, err := y.client.R().\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"token\": y.loginParam.CaptchaToken,\n\t\t\t\"REQID\": y.loginParam.ReqId,\n\t\t\t\"rnd\":   fmt.Sprint(timestamp()),\n\t\t}).\n\t\tGet(AUTH_URL + \"/api/logbox/oauth2/picCaptcha.do\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to obtain verification code\")\n\t}\n\tif imgRes.Size() > 20 {\n\t\tif setting.GetStr(conf.OcrApi) != \"\" && !y.NoUseOcr {\n\t\t\tvRes, err := base.RestyClient.R().\n\t\t\t\tSetMultipartField(\"image\", \"validateCode.png\", \"image/png\", bytes.NewReader(imgRes.Body())).\n\t\t\t\tPost(setting.GetStr(conf.OcrApi))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif jsoniter.Get(vRes.Body(), \"status\").ToInt() == 200 {\n\t\t\t\ty.VCode = jsoniter.Get(vRes.Body(), \"result\").ToString()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\t// 返回验证码图片给前端\n\t\treturn fmt.Errorf(`need img validate code: <img src=\"data:image/png;base64,%s\"/>`, base64.StdEncoding.EncodeToString(imgRes.Body()))\n\t}\n\treturn nil\n}\n\n// getQRCode 获取并返回二维码\nfunc (y *Cloud189PC) initQRCodeParam() (err error) {\n\ty.qrcodeParam = nil\n\n\tbaseParam, err := y.initBaseParams()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar qrcodeParam QRLoginParam\n\t_, err = y.client.R().\n\t\tSetFormData(map[string]string{\"appId\": APP_ID}).\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetResult(&qrcodeParam).\n\t\tPost(AUTH_URL + \"/api/logbox/oauth2/getUUID.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tqrcodeParam.BaseLoginParam = *baseParam\n\ty.qrcodeParam = &qrcodeParam\n\n\treturn y.genQRCode(\"please scan the QR code with the 189 Cloud app, then save the settings again.\")\n}\n\n// 刷新会话\nfunc (y *Cloud189PC) refreshSession() (err error) {\n\treturn y.refreshSessionWithRetry(0)\n}\n\nfunc (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) {\n\tif y.ref != nil {\n\t\treturn y.ref.refreshSessionWithRetry(retryCount)\n\t}\n\tvar erron RespErr\n\tvar userSessionResp UserSessionResp\n\t_, err = y.client.R().\n\t\tSetResult(&userSessionResp).SetError(&erron).\n\t\tSetQueryParams(clientSuffix()).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"appId\":       APP_ID,\n\t\t\t\"accessToken\": y.tokenInfo.AccessToken,\n\t\t}).\n\t\tSetHeader(\"X-Request-ID\", uuid.NewString()).\n\t\tGet(API_URL + \"/getSessionForPC.action\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// token生效刷新token\n\tif erron.HasError() {\n\t\tif erron.ResCode == UserInvalidOpenTokenError {\n\t\t\treturn y.refreshTokenWithRetry(retryCount)\n\t\t}\n\t\treturn &erron\n\t}\n\ty.tokenInfo.UserSessionResp = userSessionResp\n\treturn nil\n}\n\n// refreshToken 刷新token，失败时返回错误，不再直接调用login\nfunc (y *Cloud189PC) refreshToken() (err error) {\n\treturn y.refreshTokenWithRetry(0)\n}\n\nfunc (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {\n\tif y.ref != nil {\n\t\treturn y.ref.refreshTokenWithRetry(retryCount)\n\t}\n\n\t// 限制重试次数，避免无限递归\n\tif retryCount >= 3 {\n\t\tif y.Addition.RefreshToken != \"\" {\n\t\t\ty.Addition.RefreshToken = \"\"\n\t\t\top.MustSaveDriverStorage(y)\n\t\t}\n\t\treturn errors.New(\"refresh token failed after maximum retries\")\n\t}\n\n\tvar erron RespErr\n\tvar tokenInfo AppSessionResp\n\t_, err = y.client.R().\n\t\tSetResult(&tokenInfo).\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetError(&erron).\n\t\tSetFormData(map[string]string{\n\t\t\t\"clientId\":     APP_ID,\n\t\t\t\"refreshToken\": y.tokenInfo.RefreshToken,\n\t\t\t\"grantType\":    \"refresh_token\",\n\t\t\t\"format\":       \"json\",\n\t\t}).\n\t\tPost(AUTH_URL + \"/api/oauth2/refreshToken.do\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 如果刷新失败，返回错误给上层处理\n\tif erron.HasError() {\n\t\tif y.Addition.RefreshToken != \"\" {\n\t\t\ty.Addition.RefreshToken = \"\"\n\t\t\top.MustSaveDriverStorage(y)\n\t\t}\n\n\t\t// 根据登录类型决定下一步行为\n\t\tif y.LoginType == \"qrcode\" {\n\t\t\treturn errors.New(\"QR code session has expired, please re-scan the code to log in\")\n\t\t}\n\t\t// 密码登录模式下，尝试回退到完整登录\n\t\treturn y.login()\n\t}\n\n\ty.Addition.RefreshToken = tokenInfo.RefreshToken\n\ty.tokenInfo = &tokenInfo\n\top.MustSaveDriverStorage(y)\n\treturn y.refreshSessionWithRetry(retryCount + 1)\n}\n\nfunc (y *Cloud189PC) keepAlive() {\n\t_, err := y.get(API_URL+\"/keepUserSession.action\", func(r *resty.Request) {\n\t\tr.SetQueryParams(clientSuffix())\n\t}, nil)\n\tif err != nil {\n\t\tutils.Log.Warnf(\"189pc: Failed to keep user session alive: %v\", err)\n\t\t// 如果keepAlive失败，尝试刷新session\n\t\tif refreshErr := y.refreshSession(); refreshErr != nil {\n\t\t\tutils.Log.Errorf(\"189pc: Failed to refresh session after keepAlive error: %v\", refreshErr)\n\t\t}\n\t} else {\n\t\tutils.Log.Debugf(\"189pc: User session kept alive successfully.\")\n\t}\n}\n\n// 普通上传\n// 无法上传大小为0的文件\nfunc (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {\n\t// 文件大小\n\tfileSize := file.GetSize()\n\t// 分片大小，不得为文件大小\n\tsliceSize := partSize(fileSize)\n\n\tparams := Params{\n\t\t\"parentFolderId\": dstDir.GetID(),\n\t\t\"fileName\":       url.QueryEscape(file.GetName()),\n\t\t\"fileSize\":       fmt.Sprint(fileSize),\n\t\t\"sliceSize\":      fmt.Sprint(sliceSize), // 必须为特定分片大小\n\t\t\"lazyCheck\":      \"1\",\n\t}\n\n\tfullUrl := UPLOAD_URL\n\tif isFamily {\n\t\tparams.Set(\"familyId\", y.FamilyID)\n\t\tfullUrl += \"/family\"\n\t} else {\n\t\t// params.Set(\"extend\", `{\"opScene\":\"1\",\"relativepath\":\"\",\"rootfolderid\":\"\"}`)\n\t\tfullUrl += \"/person\"\n\t}\n\n\t// 初始化上传\n\tvar initMultiUpload InitMultiUploadResp\n\t_, err := y.request(fullUrl+\"/initMultiUpload\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, params, &initMultiUpload, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tss, err := stream.NewStreamSectionReader(file, int(sliceSize), &up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tthreadG, upCtx := errgroup.NewOrderedGroupWithContext(ctx, y.uploadThread,\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\n\tcount := 1\n\tif fileSize > sliceSize {\n\t\tcount = int((fileSize + sliceSize - 1) / sliceSize)\n\t}\n\tlastPartSize := fileSize % sliceSize\n\tif lastPartSize == 0 {\n\t\tlastPartSize = sliceSize\n\t}\n\n\tsilceMd5Hexs := make([]string, 0, count)\n\tsilceMd5 := utils.MD5.NewFunc()\n\tvar writers io.Writer = silceMd5\n\n\tfileMd5Hex := file.GetHash().GetHash(utils.MD5)\n\tvar fileMd5 hash.Hash\n\tif len(fileMd5Hex) != utils.MD5.Width {\n\t\tfileMd5 = utils.MD5.NewFunc()\n\t\twriters = io.MultiWriter(silceMd5, fileMd5)\n\t}\n\tfor i := 1; i <= count; i++ {\n\t\tif utils.IsCanceled(upCtx) {\n\t\t\tbreak\n\t\t}\n\t\toffset := int64((i)-1) * sliceSize\n\t\tpartSize := sliceSize\n\t\tif i == count {\n\t\t\tpartSize = lastPartSize\n\t\t}\n\t\tpartInfo := \"\"\n\t\tvar reader io.ReadSeeker\n\t\tthreadG.GoWithLifecycle(errgroup.Lifecycle{\n\t\t\tBefore: func(ctx context.Context) (err error) {\n\t\t\t\treader, err = ss.GetSectionReader(offset, partSize)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tsilceMd5.Reset()\n\t\t\t\tw, err := utils.CopyWithBuffer(writers, reader)\n\t\t\t\tif w != partSize {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", partSize, w, err)\n\t\t\t\t}\n\t\t\t\t// 计算块md5并进行hex和base64编码\n\t\t\t\tmd5Bytes := silceMd5.Sum(nil)\n\t\t\t\tsilceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))\n\t\t\t\tpartInfo = fmt.Sprintf(\"%d-%s\", i, base64.StdEncoding.EncodeToString(md5Bytes))\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tDo: func(ctx context.Context) (err error) {\n\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\tuploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// step.4 上传切片\n\t\t\t\tuploadUrl := uploadUrls[0]\n\t\t\t\t_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, driver.NewLimitedUploadStream(ctx, reader), isFamily)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tup(float64(threadG.Success()+1) * 100 / float64(count+1))\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tAfter: func(err error) {\n\t\t\t\tss.FreeSectionReader(reader)\n\t\t\t},\n\t\t},\n\t\t)\n\t}\n\tif err = threadG.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\tdefer up(100)\n\n\tif fileMd5 != nil {\n\t\tfileMd5Hex = strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))\n\t}\n\tsliceMd5Hex := fileMd5Hex\n\tif fileSize > sliceSize {\n\t\tsliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, \"\\n\")))\n\t}\n\n\t// 提交上传\n\tvar resp CommitMultiUploadFileResp\n\t_, err = y.request(fullUrl+\"/commitMultiUploadFile\", http.MethodGet,\n\t\tfunc(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t}, Params{\n\t\t\t\"uploadFileId\": initMultiUpload.Data.UploadFileID,\n\t\t\t\"fileMd5\":      fileMd5Hex,\n\t\t\t\"sliceMd5\":     sliceMd5Hex,\n\t\t\t\"lazyCheck\":    \"1\",\n\t\t\t\"isLog\":        \"0\",\n\t\t\t\"opertype\":     IF(overwrite, \"3\", \"1\"),\n\t\t}, &resp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.toFile(), nil\n}\n\nfunc (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {\n\tfileMd5 := stream.GetHash().GetHash(utils.MD5)\n\tif len(fileMd5) < utils.MD5.Width {\n\t\treturn nil, errors.New(\"invalid hash\")\n\t}\n\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif uploadInfo.FileDataExists != 1 {\n\t\treturn nil, errors.New(\"rapid upload fail\")\n\t}\n\n\treturn y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)\n}\n\n// 快传\nfunc (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {\n\tvar (\n\t\tcache = file.GetFile()\n\t\ttmpF  *os.File\n\t\terr   error\n\t)\n\tsize := file.GetSize()\n\tif _, ok := cache.(io.ReaderAt); !ok && size > 0 {\n\t\ttmpF, err = os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = tmpF.Close()\n\t\t\t_ = os.Remove(tmpF.Name())\n\t\t}()\n\t\tcache = tmpF\n\t}\n\tsliceSize := partSize(size)\n\tcount := 1\n\tif size > sliceSize {\n\t\tcount = int((size + sliceSize - 1) / sliceSize)\n\t}\n\tlastSliceSize := size % sliceSize\n\tif lastSliceSize == 0 {\n\t\tlastSliceSize = sliceSize\n\t}\n\n\t// step.1 优先计算所需信息\n\tbyteSize := sliceSize\n\tfileMd5 := utils.MD5.NewFunc()\n\tsliceMd5 := utils.MD5.NewFunc()\n\tsliceMd5Hexs := make([]string, 0, count)\n\tpartInfos := make([]string, 0, count)\n\twriters := []io.Writer{fileMd5, sliceMd5}\n\tif tmpF != nil {\n\t\twriters = append(writers, tmpF)\n\t}\n\twritten := int64(0)\n\tfor i := 1; i <= count; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\n\t\tif i == count {\n\t\t\tbyteSize = lastSliceSize\n\t\t}\n\n\t\tn, err := utils.CopyWithBufferN(io.MultiWriter(writers...), file, byteSize)\n\t\twritten += n\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn nil, err\n\t\t}\n\t\tmd5Byte := sliceMd5.Sum(nil)\n\t\tsliceMd5Hexs = append(sliceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))\n\t\tpartInfos = append(partInfos, fmt.Sprint(i, \"-\", base64.StdEncoding.EncodeToString(md5Byte)))\n\t\tsliceMd5.Reset()\n\t}\n\n\tif tmpF != nil {\n\t\tif size > 0 && written != size {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, incoming stream actual size= %d, expect = %d \", written, size)\n\t\t}\n\t\t_, err = tmpF.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, can't seek to 0 \")\n\t\t}\n\t}\n\n\tfileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))\n\tsliceMd5Hex := fileMd5Hex\n\tif size > sliceSize {\n\t\tsliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(sliceMd5Hexs, \"\\n\")))\n\t}\n\n\tfullUrl := UPLOAD_URL\n\tif isFamily {\n\t\tfullUrl += \"/family\"\n\t} else {\n\t\t// params.Set(\"extend\", `{\"opScene\":\"1\",\"relativepath\":\"\",\"rootfolderid\":\"\"}`)\n\t\tfullUrl += \"/person\"\n\t}\n\n\t// 尝试恢复进度\n\tuploadProgress, ok := base.GetUploadProgress[*UploadProgress](y, y.getTokenInfo().SessionKey, fileMd5Hex)\n\tif !ok {\n\t\t// step.2 预上传\n\t\tparams := Params{\n\t\t\t\"parentFolderId\": dstDir.GetID(),\n\t\t\t\"fileName\":       url.QueryEscape(file.GetName()),\n\t\t\t\"fileSize\":       fmt.Sprint(file.GetSize()),\n\t\t\t\"fileMd5\":        fileMd5Hex,\n\t\t\t\"sliceSize\":      fmt.Sprint(sliceSize),\n\t\t\t\"sliceMd5\":       sliceMd5Hex,\n\t\t}\n\t\tif isFamily {\n\t\t\tparams.Set(\"familyId\", y.FamilyID)\n\t\t}\n\t\tvar uploadInfo InitMultiUploadResp\n\t\t_, err = y.request(fullUrl+\"/initMultiUpload\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t}, params, &uploadInfo, isFamily)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuploadProgress = &UploadProgress{\n\t\t\tUploadInfo:  uploadInfo,\n\t\t\tUploadParts: partInfos,\n\t\t}\n\t}\n\n\tuploadInfo := uploadProgress.UploadInfo.Data\n\t// 网盘中不存在该文件，开始上传\n\tif uploadInfo.FileDataExists != 1 {\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, y.uploadThread,\n\t\t\tretry.Attempts(3),\n\t\t\tretry.Delay(time.Second),\n\t\t\tretry.DelayType(retry.BackOffDelay))\n\t\tfor i, uploadPart := range uploadProgress.UploadParts {\n\t\t\tif utils.IsCanceled(upCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ti, uploadPart := i, uploadPart\n\t\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t\t// step.3 获取上传链接\n\t\t\t\tuploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tuploadUrl := uploadUrls[0]\n\n\t\t\t\tbyteSize, offset := sliceSize, int64(uploadUrl.PartNumber-1)*sliceSize\n\t\t\t\tif uploadUrl.PartNumber == count {\n\t\t\t\t\tbyteSize = lastSliceSize\n\t\t\t\t}\n\n\t\t\t\t// step.4 上传切片\n\t\t\t\trateLimitedRd := driver.NewLimitedUploadStream(ctx, io.NewSectionReader(cache, offset, byteSize))\n\t\t\t\t_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, rateLimitedRd, isFamily)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tup(float64(threadG.Success()+1) * 100 / float64(len(uploadUrls)+1))\n\t\t\t\tuploadProgress.UploadParts[i] = \"\"\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\tif err = threadG.Wait(); err != nil {\n\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\tuploadProgress.UploadParts = utils.SliceFilter(uploadProgress.UploadParts, func(s string) bool { return s != \"\" })\n\t\t\t\tbase.SaveUploadProgress(y, uploadProgress, y.getTokenInfo().SessionKey, fileMd5Hex)\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer up(100)\n\t}\n\n\t// step.5 提交\n\tvar resp CommitMultiUploadFileResp\n\t_, err = y.request(fullUrl+\"/commitMultiUploadFile\", http.MethodGet,\n\t\tfunc(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t}, Params{\n\t\t\t\"uploadFileId\": uploadInfo.UploadFileID,\n\t\t\t\"isLog\":        \"0\",\n\t\t\t\"opertype\":     IF(overwrite, \"3\", \"1\"),\n\t\t}, &resp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.toFile(), nil\n}\n\n// 获取上传切片信息\n// 对http body有大小限制，分片信息太多会出错\nfunc (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {\n\tfullUrl := UPLOAD_URL\n\tif isFamily {\n\t\tfullUrl += \"/family\"\n\t} else {\n\t\tfullUrl += \"/person\"\n\t}\n\n\tvar uploadUrlsResp UploadUrlsResp\n\t_, err := y.request(fullUrl+\"/getMultiUploadUrls\", http.MethodGet,\n\t\tfunc(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t}, Params{\n\t\t\t\"uploadFileId\": uploadFileId,\n\t\t\t\"partInfo\":     strings.Join(partInfo, \",\"),\n\t\t}, &uploadUrlsResp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuploadUrls := uploadUrlsResp.Data\n\n\tif len(uploadUrls) != len(partInfo) {\n\t\treturn nil, fmt.Errorf(\"uploadUrls get error, due to get length %d, real length %d\", len(partInfo), len(uploadUrls))\n\t}\n\n\tuploadUrlInfos := make([]UploadUrlInfo, 0, len(uploadUrls))\n\tfor k, uploadUrl := range uploadUrls {\n\t\tpartNumber, err := strconv.Atoi(strings.TrimPrefix(k, \"partNumber_\"))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuploadUrlInfos = append(uploadUrlInfos, UploadUrlInfo{\n\t\t\tPartNumber:     partNumber,\n\t\t\tHeaders:        ParseHttpHeader(uploadUrl.RequestHeader),\n\t\t\tUploadUrlsData: uploadUrl,\n\t\t})\n\t}\n\tsort.Slice(uploadUrlInfos, func(i, j int) bool {\n\t\treturn uploadUrlInfos[i].PartNumber < uploadUrlInfos[j].PartNumber\n\t})\n\treturn uploadUrlInfos, nil\n}\n\n// 旧版本上传，家庭云不支持覆盖\nfunc (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {\n\ttempFile, fileMd5, err := stream.CacheFullAndHash(file, &up, utils.MD5)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trateLimited := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))\n\n\t// 创建上传会话\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 网盘中不存在该文件，开始上传\n\tstatus := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}\n\tfor status.GetSize() < file.GetSize() && status.FileDataExists != 1 {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\n\t\theader := map[string]string{\n\t\t\t\"ResumePolicy\": \"1\",\n\t\t\t\"Expect\":       \"100-continue\",\n\t\t}\n\n\t\tif isFamily {\n\t\t\theader[\"FamilyId\"] = fmt.Sprint(y.FamilyID)\n\t\t\theader[\"UploadFileId\"] = fmt.Sprint(status.UploadFileId)\n\t\t} else {\n\t\t\theader[\"Edrive-UploadFileId\"] = fmt.Sprint(status.UploadFileId)\n\t\t}\n\n\t\t_, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimited, isFamily)\n\t\tif err, ok := err.(*RespErr); ok && err.Code != \"InputStreamReadError\" {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 获取断点状态\n\t\tfullUrl := API_URL + \"/getUploadFileStatus.action\"\n\t\tif y.isFamily() {\n\t\t\tfullUrl = API_URL + \"/family/file/getFamilyFileStatus.action\"\n\t\t}\n\t\t_, err = y.get(fullUrl, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx).SetQueryParams(map[string]string{\n\t\t\t\t\"uploadFileId\": fmt.Sprint(status.UploadFileId),\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t})\n\t\t\tif isFamily {\n\t\t\t\treq.SetQueryParam(\"familyId\", fmt.Sprint(y.FamilyID))\n\t\t\t}\n\t\t}, &status, isFamily)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tup(float64(status.GetSize()) / float64(file.GetSize()) * 100)\n\t}\n\n\treturn y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)\n}\n\n// 创建上传会话\nfunc (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {\n\tvar uploadInfo CreateUploadFileResp\n\n\tfullUrl := API_URL + \"/createUploadFile.action\"\n\tif isFamily {\n\t\tfullUrl = API_URL + \"/family/file/createFamilyFile.action\"\n\t}\n\t_, err := y.post(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\tif isFamily {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"familyId\":     y.FamilyID,\n\t\t\t\t\"parentId\":     parentID,\n\t\t\t\t\"fileMd5\":      fileMd5,\n\t\t\t\t\"fileName\":     fileName,\n\t\t\t\t\"fileSize\":     fileSize,\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"parentFolderId\": parentID,\n\t\t\t\t\"fileName\":       fileName,\n\t\t\t\t\"size\":           fileSize,\n\t\t\t\t\"md5\":            fileMd5,\n\t\t\t\t\"opertype\":       \"3\",\n\t\t\t\t\"flag\":           \"1\",\n\t\t\t\t\"resumePolicy\":   \"1\",\n\t\t\t\t\"isLog\":          \"0\",\n\t\t\t})\n\t\t}\n\t}, &uploadInfo, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &uploadInfo, nil\n}\n\n// 提交上传文件\nfunc (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {\n\tvar resp OldCommitUploadFileResp\n\t_, err := y.post(fileCommitUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\tif isFamily {\n\t\t\treq.SetHeaders(map[string]string{\n\t\t\t\t\"ResumePolicy\": \"1\",\n\t\t\t\t\"UploadFileId\": fmt.Sprint(uploadFileID),\n\t\t\t\t\"FamilyId\":     fmt.Sprint(y.FamilyID),\n\t\t\t})\n\t\t} else {\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"opertype\":     IF(overwrite, \"3\", \"1\"),\n\t\t\t\t\"resumePolicy\": \"1\",\n\t\t\t\t\"uploadFileId\": fmt.Sprint(uploadFileID),\n\t\t\t\t\"isLog\":        \"0\",\n\t\t\t})\n\t\t}\n\t}, &resp, isFamily)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.toFile(), nil\n}\n\nfunc (y *Cloud189PC) isFamily() bool {\n\treturn y.Type == \"family\"\n}\n\nfunc (y *Cloud189PC) isLogin() bool {\n\tif y.tokenInfo == nil {\n\t\treturn false\n\t}\n\t_, err := y.get(API_URL+\"/getUserInfo.action\", nil, nil)\n\treturn err == nil\n}\n\n// 创建家庭云中转文件夹\nfunc (y *Cloud189PC) createFamilyTransferFolder() error {\n\tvar rootFolder Cloud189Folder\n\t_, err := y.post(API_URL+\"/family/file/createFolder.action\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"folderName\": \"FamilyTransferFolder\",\n\t\t\t\"familyId\":   y.FamilyID,\n\t\t})\n\t}, &rootFolder, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\ty.familyTransferFolder = &rootFolder\n\treturn nil\n}\n\n// 清理中转文件夹\nfunc (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {\n\ttransferFolderId := y.familyTransferFolder.GetID()\n\tfor pageNum := 1; ; pageNum++ {\n\t\tresp, err := y.getFilesWithPage(ctx, transferFolderId, true, pageNum, 100, \"lastOpTime\", \"asc\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 获取完毕跳出\n\t\tif resp.FileListAO.Count == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tvar tasks []BatchTaskInfo\n\t\tfor i := 0; i < len(resp.FileListAO.FolderList); i++ {\n\t\t\tfolder := resp.FileListAO.FolderList[i]\n\t\t\ttasks = append(tasks, BatchTaskInfo{\n\t\t\t\tFileId:   folder.GetID(),\n\t\t\t\tFileName: folder.GetName(),\n\t\t\t\tIsFolder: BoolToNumber(folder.IsDir()),\n\t\t\t})\n\t\t}\n\t\tfor i := 0; i < len(resp.FileListAO.FileList); i++ {\n\t\t\tfile := resp.FileListAO.FileList[i]\n\t\t\ttasks = append(tasks, BatchTaskInfo{\n\t\t\t\tFileId:   file.GetID(),\n\t\t\t\tFileName: file.GetName(),\n\t\t\t\tIsFolder: BoolToNumber(file.IsDir()),\n\t\t\t})\n\t\t}\n\n\t\tif len(tasks) > 0 {\n\t\t\t// 删除\n\t\t\tresp, err := y.CreateBatchTask(\"DELETE\", y.FamilyID, \"\", nil, tasks...)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = y.WaitBatchTask(\"DELETE\", resp.TaskID, time.Second)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 永久删除\n\t\t\tresp, err = y.CreateBatchTask(\"CLEAR_RECYCLE\", y.FamilyID, \"\", nil, tasks...)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = y.WaitBatchTask(\"CLEAR_RECYCLE\", resp.TaskID, time.Second)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// 获取家庭云所有用户信息\nfunc (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {\n\tvar resp FamilyInfoListResp\n\t_, err := y.get(API_URL+\"/family/manage/getFamilyList.action\", nil, &resp, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.FamilyInfoResp, nil\n}\n\n// 抽取家庭云ID\nfunc (y *Cloud189PC) getFamilyID() (string, error) {\n\tinfos, err := y.getFamilyInfoList()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(infos) == 0 {\n\t\treturn \"\", fmt.Errorf(\"cannot get automatically,please input family_id\")\n\t}\n\tfor _, info := range infos {\n\t\tif strings.Contains(y.getTokenInfo().LoginName, info.RemarkName) {\n\t\t\treturn fmt.Sprint(info.FamilyID), nil\n\t\t}\n\t}\n\treturn fmt.Sprint(infos[0].FamilyID), nil\n}\n\n// 保存家庭云中的文件到个人云\nfunc (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId string, srcObj, dstDir model.Obj, overwrite bool) error {\n\t// _, err := y.post(API_URL+\"/family/file/saveFileToMember.action\", func(req *resty.Request) {\n\t// \treq.SetQueryParams(map[string]string{\n\t// \t\t\"channelId\":    \"home\",\n\t// \t\t\"familyId\":     familyId,\n\t// \t\t\"destParentId\": destParentId,\n\t// \t\t\"fileIdList\":   familyFileId,\n\t// \t})\n\t// }, nil)\n\t// return err\n\n\ttask := BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t}\n\tresp, err := y.CreateBatchTask(\"COPY\", familyId, dstDir.GetID(), map[string]string{\n\t\t\"groupId\":  \"null\",\n\t\t\"copyType\": \"2\",\n\t\t\"shareId\":  \"null\",\n\t}, task)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tstate, err := y.CheckBatchTask(\"COPY\", resp.TaskID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch state.TaskStatus {\n\t\tcase 2:\n\t\t\ttask.DealWay = IF(overwrite, 3, 2)\n\t\t\t// 冲突时覆盖文件\n\t\t\tif err := y.ManageBatchTask(\"COPY\", resp.TaskID, dstDir.GetID(), task); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase 4:\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(time.Millisecond * 400)\n\t}\n}\n\n// 永久删除文件\nfunc (y *Cloud189PC) Delete(ctx context.Context, familyId string, srcObj model.Obj) error {\n\ttask := BatchTaskInfo{\n\t\tFileId:   srcObj.GetID(),\n\t\tFileName: srcObj.GetName(),\n\t\tIsFolder: BoolToNumber(srcObj.IsDir()),\n\t}\n\t// 删除源文件\n\tresp, err := y.CreateBatchTask(\"DELETE\", familyId, \"\", nil, task)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = y.WaitBatchTask(\"DELETE\", resp.TaskID, time.Second)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 清除回收站\n\tresp, err = y.CreateBatchTask(\"CLEAR_RECYCLE\", familyId, \"\", nil, task)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = y.WaitBatchTask(\"CLEAR_RECYCLE\", resp.TaskID, time.Second)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {\n\tvar resp CreateBatchTaskResp\n\t_, err := y.post(API_URL+\"/batch/createBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":      aType,\n\t\t\t\"taskInfos\": MustString(utils.Json.MarshalToString(taskInfos)),\n\t\t})\n\t\tif targetFolderId != \"\" {\n\t\t\treq.SetFormData(map[string]string{\"targetFolderId\": targetFolderId})\n\t\t}\n\t\tif familyID != \"\" {\n\t\t\treq.SetFormData(map[string]string{\"familyId\": familyID})\n\t\t}\n\t\treq.SetFormData(other)\n\t}, &resp, familyID != \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 检测任务状态\nfunc (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {\n\tvar resp BatchTaskStateResp\n\t_, err := y.post(API_URL+\"/batch/checkBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":   aType,\n\t\t\t\"taskId\": taskID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 获取冲突的任务信息\nfunc (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {\n\tvar resp BatchTaskConflictTaskInfoResp\n\t_, err := y.post(API_URL+\"/batch/getConflictTaskInfo.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"type\":   aType,\n\t\t\t\"taskId\": taskID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\n// 处理冲突\nfunc (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {\n\t_, err := y.post(API_URL+\"/batch/manageBatchTask.action\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"targetFolderId\": targetFolderId,\n\t\t\t\"type\":           aType,\n\t\t\t\"taskId\":         taskID,\n\t\t\t\"taskInfos\":      MustString(utils.Json.MarshalToString(taskInfos)),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nvar ErrIsConflict = errors.New(\"there is a conflict with the target object\")\n\n// 等待任务完成\nfunc (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {\n\tfor {\n\t\tstate, err := y.CheckBatchTask(aType, taskID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch state.TaskStatus {\n\t\tcase 2:\n\t\t\treturn ErrIsConflict\n\t\tcase 4:\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(t)\n\t}\n}\n\nfunc (y *Cloud189PC) getTokenInfo() *AppSessionResp {\n\tif y.ref != nil {\n\t\treturn y.ref.getTokenInfo()\n\t}\n\treturn y.tokenInfo\n}\n\nfunc (y *Cloud189PC) getClient() *resty.Client {\n\tif y.ref != nil {\n\t\treturn y.ref.getClient()\n\t}\n\treturn y.client\n}\n\nfunc (y *Cloud189PC) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {\n\tfullUrl := API_URL + \"/portal/getUserSizeInfo.action\"\n\tvar resp CapacityResp\n\t_, err := y.get(fullUrl, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/alias/driver.go",
    "content": "package alias\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\ntype Alias struct {\n\tmodel.Storage\n\tAddition\n\trootOrder []string\n\tpathMap   map[string][]string\n\troot      model.Obj\n}\n\nfunc (d *Alias) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Alias) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Alias) Init(ctx context.Context) error {\n\tpaths := strings.Split(d.Paths, \"\\n\")\n\td.rootOrder = make([]string, 0, len(paths))\n\td.pathMap = make(map[string][]string)\n\tfor _, path := range paths {\n\t\tpath = strings.TrimSpace(path)\n\t\tif path == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tk, v := getPair(path)\n\t\ttemp, ok := d.pathMap[k]\n\t\tif !ok {\n\t\t\td.rootOrder = append(d.rootOrder, k)\n\t\t}\n\t\td.pathMap[k] = append(temp, v)\n\t}\n\n\tswitch len(d.rootOrder) {\n\tcase 0:\n\t\treturn errors.New(\"paths is required\")\n\tcase 1:\n\t\tpaths := d.pathMap[d.rootOrder[0]]\n\t\troots := make(BalancedObjs, 0, len(paths))\n\t\troots = append(roots, &model.Object{\n\t\t\tName:     \"root\",\n\t\t\tPath:     paths[0],\n\t\t\tIsFolder: true,\n\t\t\tModified: d.Modified,\n\t\t\tMask:     model.Locked,\n\t\t})\n\t\tfor _, path := range paths[1:] {\n\t\t\troots = append(roots, &model.Object{\n\t\t\t\tPath: path,\n\t\t\t})\n\t\t}\n\t\td.root = roots\n\tdefault:\n\t\td.root = &model.Object{\n\t\t\tName:     \"root\",\n\t\t\tPath:     \"/\",\n\t\t\tIsFolder: true,\n\t\t\tModified: d.Modified,\n\t\t\tMask:     model.ReadOnly,\n\t\t}\n\t}\n\n\tif !utils.SliceContains(ValidReadConflictPolicy, d.ReadConflictPolicy) {\n\t\td.ReadConflictPolicy = FirstRWP\n\t}\n\tif !utils.SliceContains(ValidWriteConflictPolicy, d.WriteConflictPolicy) {\n\t\td.WriteConflictPolicy = DisabledWP\n\t}\n\tif !utils.SliceContains(ValidPutConflictPolicy, d.PutConflictPolicy) {\n\t\td.PutConflictPolicy = DisabledWP\n\t}\n\treturn nil\n}\n\nfunc (d *Alias) Drop(ctx context.Context) error {\n\td.rootOrder = nil\n\td.pathMap = nil\n\td.root = nil\n\treturn nil\n}\n\nfunc (d *Alias) GetRoot(ctx context.Context) (model.Obj, error) {\n\tif d.root == nil {\n\t\treturn nil, errs.StorageNotInit\n\t}\n\treturn d.root, nil\n}\n\n// 通过op.Get调用的话，path一定是子路径(/开头)\nfunc (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {\n\troots, sub := d.getRootsAndPath(path)\n\tif len(roots) == 0 {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor idx, root := range roots {\n\t\trawPath := stdpath.Join(root, sub)\n\t\tobj, err := fs.Get(ctx, rawPath, &fs.GetArgs{NoLog: true})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tmask := model.GetObjMask(obj) &^ model.Temp\n\t\tif sub == \"\" {\n\t\t\t// 根目录\n\t\t\tmask |= model.Locked | model.Virtual\n\t\t}\n\t\tret := model.Object{\n\t\t\tPath:     rawPath,\n\t\t\tName:     obj.GetName(),\n\t\t\tSize:     obj.GetSize(),\n\t\t\tModified: obj.ModTime(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t\tHashInfo: obj.GetHash(),\n\t\t\tMask:     mask,\n\t\t}\n\t\tobj = &ret\n\t\tif d.ProviderPassThrough && !obj.IsDir() {\n\t\t\tif storage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{}); err == nil {\n\t\t\t\tobj = &model.ObjectProvider{\n\t\t\t\t\tObject: ret,\n\t\t\t\t\tProvider: model.Provider{\n\t\t\t\t\t\tProvider: storage.Config().Name,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\troots = roots[idx+1:]\n\t\tvar objs BalancedObjs\n\t\tif idx > 0 {\n\t\t\tobjs = make(BalancedObjs, 0, len(roots)+2)\n\t\t} else {\n\t\t\tobjs = make(BalancedObjs, 0, len(roots)+1)\n\t\t}\n\t\tobjs = append(objs, obj)\n\t\tif idx > 0 {\n\t\t\tobjs = append(objs, nil)\n\t\t}\n\t\tfor _, d := range roots {\n\t\t\tobjs = append(objs, &tempObj{model.Object{\n\t\t\t\tPath: stdpath.Join(d, sub),\n\t\t\t}})\n\t\t}\n\t\treturn objs, nil\n\t}\n\treturn nil, errs.ObjectNotFound\n}\n\nfunc (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tdirs, ok := dir.(BalancedObjs)\n\tif !ok {\n\t\treturn d.listRoot(ctx, args.WithStorageDetails && d.DetailsPassThrough, args.Refresh), nil\n\t}\n\n\t// 因为alias是NoCache且Get方法不会返回NotSupport或NotImplement错误\n\t// 所以这里对象不会传回到alias，也就不需要返回BalancedObjs了\n\tobjMap := make(map[string]model.Obj)\n\tfor _, dir := range dirs {\n\t\tif dir == nil {\n\t\t\tcontinue\n\t\t}\n\t\tdirPath := dir.GetPath()\n\t\ttmp, err := fs.List(ctx, dirPath, &fs.ListArgs{\n\t\t\tNoLog:              true,\n\t\t\tRefresh:            args.Refresh,\n\t\t\tWithStorageDetails: args.WithStorageDetails && d.DetailsPassThrough,\n\t\t})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, obj := range tmp {\n\t\t\tname := obj.GetName()\n\t\t\tif _, exists := objMap[name]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmask := model.GetObjMask(obj) &^ model.Temp\n\t\t\tobjRes := model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tPath:     stdpath.Join(dirPath, name),\n\t\t\t\tSize:     obj.GetSize(),\n\t\t\t\tModified: obj.ModTime(),\n\t\t\t\tIsFolder: obj.IsDir(),\n\t\t\t\tMask:     mask,\n\t\t\t}\n\t\t\tvar objRet model.Obj\n\t\t\tif thumb, ok := model.GetThumb(obj); ok {\n\t\t\t\tobjRet = &model.ObjThumb{\n\t\t\t\t\tObject: objRes,\n\t\t\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\t\t\tThumbnail: thumb,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tobjRet = &objRes\n\t\t\t}\n\t\t\tif details, ok := model.GetStorageDetails(obj); ok {\n\t\t\t\tobjRet = &model.ObjStorageDetails{\n\t\t\t\t\tObj:            objRet,\n\t\t\t\t\tStorageDetails: details,\n\t\t\t\t}\n\t\t\t}\n\t\t\tobjMap[name] = objRet\n\t\t}\n\t}\n\tobjs := make([]model.Obj, 0, len(objMap))\n\tfor _, obj := range objMap {\n\t\tobjs = append(objs, obj)\n\t}\n\tif d.OrderBy == \"\" {\n\t\tsort := getAllSort(dirs)\n\t\tif sort.OrderBy != \"\" {\n\t\t\tmodel.SortFiles(objs, sort.OrderBy, sort.OrderDirection)\n\t\t}\n\t\tif d.ExtractFolder == \"\" && sort.ExtractFolder != \"\" {\n\t\t\tmodel.ExtractFolder(objs, sort.ExtractFolder)\n\t\t}\n\t}\n\treturn objs, nil\n}\n\nfunc (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.ReadConflictPolicy == AllRWP && !args.Redirect {\n\t\tfiles, err := d.getAllObjs(ctx, file, getWriteAndPutFilterFunc(AllRWP))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlinkClosers := make([]io.Closer, 0, len(files))\n\t\trrf := make([]model.RangeReaderIF, 0, len(files))\n\t\tfor _, f := range files {\n\t\t\tlink, fi, err := d.link(ctx, f.GetPath(), args)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif fi.GetSize() != files.GetSize() {\n\t\t\t\t_ = link.Close()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tl := *link // 复制一份，避免修改到原始link\n\t\t\tif l.ContentLength == 0 {\n\t\t\t\tl.ContentLength = fi.GetSize()\n\t\t\t}\n\t\t\tif d.DownloadConcurrency > 0 {\n\t\t\t\tl.Concurrency = d.DownloadConcurrency\n\t\t\t}\n\t\t\tif d.DownloadPartSize > 0 {\n\t\t\t\tl.PartSize = d.DownloadPartSize * utils.KB\n\t\t\t}\n\t\t\trr, err := stream.GetRangeReaderFromLink(l.ContentLength, &l)\n\t\t\tif err != nil {\n\t\t\t\t_ = link.Close()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlinkClosers = append(linkClosers, link)\n\t\t\trrf = append(rrf, rr)\n\t\t}\n\t\trr := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\treturn rrf[rand.Intn(len(rrf))].RangeRead(ctx, httpRange)\n\t\t}\n\t\treturn &model.Link{\n\t\t\tRangeReader: stream.RangeReaderFunc(rr),\n\t\t\tSyncClosers: utils.NewSyncClosers(linkClosers...),\n\t\t}, nil\n\t}\n\n\tvar link *model.Link\n\tvar fi model.Obj\n\tvar err error\n\tfiles := file.(BalancedObjs)\n\tif d.ReadConflictPolicy == RandomBalancedRP || d.ReadConflictPolicy == AllRWP {\n\t\trand.Shuffle(len(files), func(i, j int) {\n\t\t\tfiles[i], files[j] = files[j], files[i]\n\t\t})\n\t}\n\tfor _, f := range files {\n\t\tif f == nil {\n\t\t\tcontinue\n\t\t}\n\t\tlink, fi, err = d.link(ctx, f.GetPath(), args)\n\t\tif err == nil {\n\t\t\tif link == nil {\n\t\t\t\t// 重定向且需要通过代理\n\t\t\t\treturn &model.Link{\n\t\t\t\t\tURL: fmt.Sprintf(\"%s/p%s?sign=%s\",\n\t\t\t\t\t\tcommon.GetApiUrl(ctx),\n\t\t\t\t\t\tutils.EncodePath(f.GetPath(), true),\n\t\t\t\t\t\tsign.Sign(f.GetPath())),\n\t\t\t\t}, nil\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresultLink := *link // 复制一份，避免修改到原始link\n\tresultLink.Expiration = nil\n\tresultLink.SyncClosers = utils.NewSyncClosers(link)\n\tif args.Redirect {\n\t\treturn &resultLink, nil\n\t}\n\tif resultLink.ContentLength == 0 {\n\t\tresultLink.ContentLength = fi.GetSize()\n\t}\n\tif d.DownloadConcurrency > 0 {\n\t\tresultLink.Concurrency = d.DownloadConcurrency\n\t}\n\tif d.DownloadPartSize > 0 {\n\t\tresultLink.PartSize = d.DownloadPartSize * utils.KB\n\t}\n\treturn &resultLink, nil\n}\n\nfunc (d *Alias) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\t// Other 不应负载均衡，这是因为前端是否调用 /fs/other 的判断条件是返回的 provider 的值\n\t// 而 ProviderPassThrough 开启时，返回的 provider 固定为第一个 obj 的后端驱动\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.Obj.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn op.Other(ctx, storage, model.FsOtherArgs{\n\t\tPath:   actualPath,\n\t\tMethod: args.Method,\n\t\tData:   args.Data,\n\t})\n}\n\nfunc (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tobjs, err := d.getWriteObjs(ctx, parentDir)\n\tif err == nil {\n\t\tfor _, obj := range objs {\n\t\t\terr = errors.Join(err, fs.MakeDir(ctx, stdpath.Join(obj.GetPath(), dirName)))\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcs, dsts, err := d.getMoveObjs(ctx, srcObj, dstDir)\n\tif err == nil {\n\t\tfor i, dst := range dsts {\n\t\t\tsrc := srcs[i]\n\t\t\t_, e := fs.Move(ctx, src.GetPath(), dst.GetPath())\n\t\t\terr = errors.Join(err, e)\n\t\t}\n\t\tsrcs = srcs[len(dsts):]\n\t\tfor _, src := range srcs {\n\t\t\te := fs.Remove(ctx, src.GetPath())\n\t\t\terr = errors.Join(err, e)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tobjs, err := d.getWriteObjs(ctx, srcObj)\n\tif err == nil {\n\t\tfor _, obj := range objs {\n\t\t\terr = errors.Join(err, fs.Rename(ctx, obj.GetPath(), newName))\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcs, dsts, err := d.getCopyObjs(ctx, srcObj, dstDir)\n\tif err == nil {\n\t\tfor i, src := range srcs {\n\t\t\tdst := dsts[i]\n\t\t\t_, e := fs.Copy(ctx, src.GetPath(), dst.GetPath())\n\t\t\terr = errors.Join(err, e)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Remove(ctx context.Context, obj model.Obj) error {\n\tobjs, err := d.getWriteObjs(ctx, obj)\n\tif err == nil {\n\t\tfor _, obj := range objs {\n\t\t\terr = errors.Join(err, fs.Remove(ctx, obj.GetPath()))\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tobjs, err := d.getPutObjs(ctx, dstDir)\n\tif err == nil {\n\t\tif len(objs) == 1 {\n\t\t\tstorage, reqActualPath, err := op.GetStorageAndActualPath(objs.GetPath())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn op.Put(ctx, storage, reqActualPath, &stream.FileStream{\n\t\t\t\tObj:      s,\n\t\t\t\tMimetype: s.GetMimetype(),\n\t\t\t\tReader:   s,\n\t\t\t}, up)\n\t\t} else {\n\t\t\tfile, err := s.CacheFullAndWriter(nil, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcount := float64(len(objs) + 1)\n\t\t\tup(100 / count)\n\t\t\tfor i, obj := range objs {\n\t\t\t\terr = errors.Join(err, fs.PutDirectly(ctx, obj.GetPath(), &stream.FileStream{\n\t\t\t\t\tObj:      s,\n\t\t\t\t\tMimetype: s.GetMimetype(),\n\t\t\t\t\tReader:   file,\n\t\t\t\t}))\n\t\t\t\tup(float64(i+2) / float64(count) * 100)\n\t\t\t\t_, e := file.Seek(0, io.SeekStart)\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn errors.Join(err, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) PutURL(ctx context.Context, dstDir model.Obj, name, url string) error {\n\tobjs, err := d.getPutObjs(ctx, dstDir)\n\tif err == nil {\n\t\tfor _, obj := range objs {\n\t\t\terr = errors.Join(err, fs.PutURL(ctx, obj.GetPath(), name, url))\n\t\t}\n\t\treturn err\n\t}\n\treturn err\n}\n\nfunc (d *Alias) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treqPath := d.getBalancedPath(ctx, obj)\n\tif reqPath == \"\" {\n\t\treturn nil, errs.NotFile\n\t}\n\tmeta, err := d.getArchiveMeta(ctx, reqPath, args)\n\tif err == nil {\n\t\treturn meta, nil\n\t}\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Alias) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treqPath := d.getBalancedPath(ctx, obj)\n\tif reqPath == \"\" {\n\t\treturn nil, errs.NotFile\n\t}\n\tl, err := d.listArchive(ctx, reqPath, args)\n\tif err == nil {\n\t\treturn l, nil\n\t}\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Alias) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// alias的两个驱动，一个支持驱动提取，一个不支持，如何兼容？\n\t// 如果访问的是不支持驱动提取的驱动内的压缩文件，GetArchiveMeta就会返回errs.NotImplement，提取URL前缀就会是/ae，Extract就不会被调用\n\t// 如果访问的是支持驱动提取的驱动内的压缩文件，GetArchiveMeta就会返回有效值，提取URL前缀就会是/ad，Extract就会被调用\n\treqPath := d.getBalancedPath(ctx, obj)\n\tif reqPath == \"\" {\n\t\treturn nil, errs.NotFile\n\t}\n\tlink, err := d.extract(ctx, reqPath, args)\n\tif err != nil {\n\t\treturn nil, errs.NotImplement\n\t}\n\tif link == nil {\n\t\treturn &model.Link{\n\t\t\tURL: fmt.Sprintf(\"%s/ap%s?inner=%s&pass=%s&sign=%s\",\n\t\t\t\tcommon.GetApiUrl(ctx),\n\t\t\t\tutils.EncodePath(reqPath, true),\n\t\t\t\tutils.EncodePath(args.InnerPath, true),\n\t\t\t\turl.QueryEscape(args.Password),\n\t\t\t\tsign.SignArchive(reqPath)),\n\t\t}, nil\n\t}\n\tresultLink := *link\n\tresultLink.SyncClosers = utils.NewSyncClosers(link)\n\treturn &resultLink, nil\n}\n\nfunc (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {\n\tsrcs, dsts, err := d.getCopyObjs(ctx, srcObj, dstDir)\n\tif err == nil {\n\t\tfor i, src := range srcs {\n\t\t\tdst := dsts[i]\n\t\t\t_, e := fs.ArchiveDecompress(ctx, src.GetPath(), dst.GetPath(), args)\n\t\t\terr = errors.Join(err, e)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Alias) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif !d.DetailsPassThrough {\n\t\treturn nil, errs.NotImplement\n\t}\n\tif len(d.rootOrder) != 1 {\n\t\treturn nil, errs.NotImplement\n\t}\n\tbackends := d.pathMap[d.rootOrder[0]]\n\tvar storage driver.Driver\n\tfor _, backend := range backends {\n\t\ts, err := fs.GetStorage(backend, &fs.GetStoragesArgs{})\n\t\tif err != nil {\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\t\tif storage == nil {\n\t\t\tstorage = s\n\t\t} else if storage.GetStorage().MountPath != s.GetStorage().MountPath {\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\t}\n\tif storage == nil { // should never access\n\t\treturn nil, errs.NotImplement\n\t}\n\treturn op.GetStorageDetails(ctx, storage)\n}\n\nfunc (d *Alias) ResolveLinkCacheMode(path string) driver.LinkCacheMode {\n\troots, sub := d.getRootsAndPath(path)\n\tif len(roots) == 0 {\n\t\treturn 0\n\t}\n\tfor _, root := range roots {\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(stdpath.Join(root, sub))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcontinue\n\t\t}\n\t\tmode := storage.Config().LinkCacheMode\n\t\tif mode == -1 {\n\t\t\treturn storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(actualPath)\n\t\t} else {\n\t\t\treturn mode\n\t\t}\n\t}\n\treturn 0\n}\n\nvar _ driver.Driver = (*Alias)(nil)\n"
  },
  {
    "path": "drivers/alias/meta.go",
    "content": "package alias\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tPaths                string `json:\"paths\" required:\"true\" type:\"text\"`\n\tReadConflictPolicy   string `json:\"read_conflict_policy\" type:\"select\" options:\"first,random,all\" default:\"first\"`\n\tWriteConflictPolicy  string `json:\"write_conflict_policy\" type:\"select\" options:\"disabled,first,deterministic,deterministic_or_all,all,all_strict\" default:\"disabled\" help:\"How the driver handles identical backend paths when renaming, removing, or making directories.\"`\n\tPutConflictPolicy    string `json:\"put_conflict_policy\" type:\"select\" options:\"disabled,first,deterministic,deterministic_or_all,all,all_strict,random,quota,quota_strict\" default:\"disabled\" help:\"How the driver handles identical backend paths when uploading, copying, moving, or decompressing.\"`\n\tFileConsistencyCheck bool   `json:\"file_consistency_check\" type:\"bool\" default:\"false\"`\n\tDownloadConcurrency  int    `json:\"download_concurrency\" default:\"0\" required:\"false\" type:\"number\" help:\"Need to enable proxy\"`\n\tDownloadPartSize     int    `json:\"download_part_size\" default:\"0\" type:\"number\" required:\"false\" help:\"Need to enable proxy. Unit: KB\"`\n\tProviderPassThrough  bool   `json:\"provider_pass_through\" type:\"bool\" default:\"false\"`\n\tDetailsPassThrough   bool   `json:\"details_pass_through\" type:\"bool\" default:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:             \"Alias\",\n\tLocalSort:        true,\n\tNoCache:          true,\n\tNoUpload:         false,\n\tDefaultRoot:      \"/\",\n\tProxyRangeOption: true,\n\tLinkCacheMode:    driver.LinkCacheAuto,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Alias{}\n\t})\n}\n"
  },
  {
    "path": "drivers/alias/types.go",
    "content": "package alias\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nconst (\n\tDisabledWP             = \"disabled\"\n\tFirstRWP               = \"first\"\n\tDeterministicWP        = \"deterministic\"\n\tDeterministicOrAllWP   = \"deterministic_or_all\"\n\tAllRWP                 = \"all\"\n\tAllStrictWP            = \"all_strict\"\n\tRandomBalancedRP       = \"random\"\n\tBalancedByQuotaP       = \"quota\"\n\tBalancedByQuotaStrictP = \"quota_strict\"\n)\n\nvar (\n\tValidReadConflictPolicy  = []string{FirstRWP, RandomBalancedRP, AllRWP}\n\tValidWriteConflictPolicy = []string{DisabledWP, FirstRWP, DeterministicWP, DeterministicOrAllWP, AllRWP,\n\t\tAllStrictWP}\n\tValidPutConflictPolicy = []string{DisabledWP, FirstRWP, DeterministicWP, DeterministicOrAllWP, AllRWP,\n\t\tAllStrictWP, RandomBalancedRP, BalancedByQuotaP, BalancedByQuotaStrictP}\n)\n\nvar (\n\tErrPathConflict     = errors.New(\"path conflict\")\n\tErrSamePathLeak     = errors.New(\"leak some of same-name dirs\")\n\tErrNoEnoughSpace    = errors.New(\"none of same-name dirs has enough space\")\n\tErrNotEnoughSrcObjs = errors.New(\"cannot move fewer objs to more paths, please try copying\")\n)\n\ntype BalancedObjs []model.Obj\n\nfunc (b BalancedObjs) GetSize() int64 {\n\treturn b[0].GetSize()\n}\n\nfunc (b BalancedObjs) ModTime() time.Time {\n\treturn b[0].ModTime()\n}\n\nfunc (b BalancedObjs) CreateTime() time.Time {\n\treturn b[0].CreateTime()\n}\n\nfunc (b BalancedObjs) IsDir() bool {\n\treturn b[0].IsDir()\n}\n\nfunc (b BalancedObjs) GetHash() utils.HashInfo {\n\treturn b[0].GetHash()\n}\n\nfunc (b BalancedObjs) GetName() string {\n\treturn b[0].GetName()\n}\n\nfunc (b BalancedObjs) GetPath() string {\n\treturn b[0].GetPath()\n}\n\nfunc (b BalancedObjs) GetID() string {\n\treturn b[0].GetID()\n}\n\nfunc (b BalancedObjs) Unwrap() model.Obj {\n\treturn b[0]\n}\n\nvar _ model.Obj = (BalancedObjs)(nil)\n\ntype tempObj struct{ model.Object }\n"
  },
  {
    "path": "drivers/alias/util.go",
    "content": "package alias\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype detailWithIndex struct {\n\tidx int\n\tval *model.StorageDetails\n}\n\nfunc (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj {\n\tvar objs []model.Obj\n\tdetailsChan := make(chan detailWithIndex, len(d.pathMap))\n\tworkerCount := 0\n\tfor _, k := range d.rootOrder {\n\t\tobj := &model.Object{\n\t\t\tName:     k,\n\t\t\tPath:     \"/\" + k,\n\t\t\tIsFolder: true,\n\t\t\tModified: d.Modified,\n\t\t\tMask:     model.Locked | model.Virtual,\n\t\t}\n\t\tidx := len(objs)\n\t\tobjs = append(objs, obj)\n\t\tv := d.pathMap[k]\n\t\tif !withDetails || len(v) != 1 {\n\t\t\tcontinue\n\t\t}\n\t\tremoteDriver, err := op.GetStorageByMountPath(v[0])\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tobj.Modified = remoteDriver.GetStorage().Modified\n\t\t_, ok := remoteDriver.(driver.WithDetails)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tobjs[idx] = &model.ObjStorageDetails{\n\t\t\tObj:            objs[idx],\n\t\t\tStorageDetails: nil,\n\t\t}\n\t\tworkerCount++\n\t\tgo func(dri driver.Driver, i int) {\n\t\t\tdetails, e := op.GetStorageDetails(ctx, dri, refresh)\n\t\t\tif e != nil {\n\t\t\t\tif !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {\n\t\t\t\t\tlog.Errorf(\"failed get %s storage details: %+v\", dri.GetStorage().MountPath, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdetailsChan <- detailWithIndex{idx: i, val: details}\n\t\t}(remoteDriver, idx)\n\t}\n\tfor workerCount > 0 {\n\t\tselect {\n\t\tcase r := <-detailsChan:\n\t\t\tobjs[r.idx].(*model.ObjStorageDetails).StorageDetails = r.val\n\t\t\tworkerCount--\n\t\tcase <-time.After(time.Second):\n\t\t\tworkerCount = 0\n\t\t}\n\t}\n\treturn objs\n}\n\n// do others that not defined in Driver interface\nfunc getPair(path string) (string, string) {\n\tif name, path, ok := strings.Cut(path, \":\"); ok && !strings.Contains(name, \"/\") {\n\t\treturn name, path\n\t}\n\treturn stdpath.Base(path), path\n}\n\nfunc (d *Alias) getRootsAndPath(path string) (roots []string, sub string) {\n\tif len(d.rootOrder) == 1 {\n\t\treturn d.pathMap[d.rootOrder[0]], path\n\t}\n\tpath = strings.TrimPrefix(path, \"/\")\n\tbefore, after, ok := strings.Cut(path, \"/\")\n\tif !ok {\n\t\treturn d.pathMap[path], \"\"\n\t}\n\treturn d.pathMap[before], after\n}\n\nfunc (d *Alias) link(ctx context.Context, reqPath string, args model.LinkArgs) (*model.Link, model.Obj, error) {\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif args.Redirect && common.ShouldProxy(storage, stdpath.Base(reqPath)) {\n\t\treturn nil, nil, nil\n\t}\n\treturn op.Link(ctx, storage, reqActualPath, args)\n}\n\nfunc isConsistent(a, b model.Obj) bool {\n\tif a.GetSize() != b.GetSize() {\n\t\treturn false\n\t}\n\tfor ht, v := range a.GetHash().All() {\n\t\tah := b.GetHash().GetHash(ht)\n\t\tif ah != \"\" && ah != v {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (d *Alias) getAllObjs(ctx context.Context, bObj model.Obj, ifContinue func(err error) (bool, error)) (BalancedObjs, error) {\n\tobjs := bObj.(BalancedObjs)\n\tlength := 0\n\tfor _, o := range objs {\n\t\tvar err error\n\t\tvar obj model.Obj\n\t\ttemp, isTemp := o.(*tempObj)\n\t\tif isTemp {\n\t\t\tobj, err = fs.Get(ctx, o.GetPath(), &fs.GetArgs{NoLog: true})\n\t\t\tif err == nil {\n\t\t\t\tif !bObj.IsDir() {\n\t\t\t\t\tif obj.IsDir() {\n\t\t\t\t\t\terr = errs.NotFile\n\t\t\t\t\t} else if d.FileConsistencyCheck && !isConsistent(bObj, obj) {\n\t\t\t\t\t\terr = errs.ObjectNotFound\n\t\t\t\t\t}\n\t\t\t\t} else if !obj.IsDir() {\n\t\t\t\t\terr = errs.NotFolder\n\t\t\t\t}\n\t\t\t}\n\t\t} else if o == nil {\n\t\t\terr = errs.ObjectNotFound\n\t\t}\n\n\t\tcont, err := ifContinue(err)\n\t\tif err != nil {\n\t\t\tif cont {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tif isTemp {\n\t\t\tobjRes := temp.Object\n\t\t\t// objRes.Name = obj.GetName()\n\t\t\t// objRes.Size = obj.GetSize()\n\t\t\t// objRes.Modified = obj.ModTime()\n\t\t\t// objRes.HashInfo = obj.GetHash()\n\t\t\tobjs[length] = &objRes\n\t\t} else {\n\t\t\tobjs[length] = o\n\t\t}\n\t\tlength++\n\t\tif !cont {\n\t\t\tbreak\n\t\t}\n\t}\n\tif length == 0 {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\treturn objs[:length], nil\n}\n\nfunc (d *Alias) getBalancedPath(ctx context.Context, file model.Obj) string {\n\tif d.ReadConflictPolicy == FirstRWP {\n\t\treturn file.GetPath()\n\t}\n\tfiles := file.(BalancedObjs)\n\tif rand.Intn(len(files)) == 0 {\n\t\treturn file.GetPath()\n\t}\n\tfiles, _ = d.getAllObjs(ctx, file, getWriteAndPutFilterFunc(AllRWP))\n\treturn files[rand.Intn(len(files))].GetPath()\n}\n\nfunc getWriteAndPutFilterFunc(policy string) func(error) (bool, error) {\n\tif policy == AllRWP {\n\t\treturn func(err error) (bool, error) {\n\t\t\treturn true, err\n\t\t}\n\t}\n\tall := true\n\tl := 0\n\treturn func(err error) (bool, error) {\n\t\tif err != nil {\n\t\t\tswitch policy {\n\t\t\tcase AllStrictWP:\n\t\t\t\treturn false, ErrSamePathLeak\n\t\t\tcase DeterministicOrAllWP:\n\t\t\t\tif l >= 2 {\n\t\t\t\t\treturn false, ErrSamePathLeak\n\t\t\t\t}\n\t\t\t}\n\t\t\tall = false\n\t\t} else {\n\t\t\tswitch policy {\n\t\t\tcase FirstRWP:\n\t\t\t\treturn false, nil\n\t\t\tcase DeterministicWP:\n\t\t\t\tif l > 0 {\n\t\t\t\t\treturn false, ErrPathConflict\n\t\t\t\t}\n\t\t\tcase DeterministicOrAllWP:\n\t\t\t\tif l > 0 && !all {\n\t\t\t\t\treturn false, ErrSamePathLeak\n\t\t\t\t}\n\t\t\t}\n\t\t\tl += 1\n\t\t}\n\t\treturn true, err\n\t}\n}\n\nfunc (d *Alias) getWriteObjs(ctx context.Context, obj model.Obj) (BalancedObjs, error) {\n\tif d.WriteConflictPolicy == DisabledWP {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\treturn d.getAllObjs(ctx, obj, getWriteAndPutFilterFunc(d.WriteConflictPolicy))\n}\n\nfunc (d *Alias) getPutObjs(ctx context.Context, obj model.Obj) (BalancedObjs, error) {\n\tif d.PutConflictPolicy == DisabledWP {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\tobjs, err := d.getAllObjs(ctx, obj, getWriteAndPutFilterFunc(d.PutConflictPolicy))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstrict := false\n\tswitch d.PutConflictPolicy {\n\tcase RandomBalancedRP:\n\t\tri := rand.Intn(len(objs))\n\t\treturn objs[ri : ri+1], nil\n\tcase BalancedByQuotaStrictP:\n\t\tstrict = true\n\t\tfallthrough\n\tcase BalancedByQuotaP:\n\t\tobjs, ok := getRandomObjByQuotaBalanced(ctx, objs, strict, obj.GetSize())\n\t\tif !ok {\n\t\t\treturn nil, ErrNoEnoughSpace\n\t\t}\n\t\treturn objs, nil\n\tdefault:\n\t\treturn objs, nil\n\t}\n}\n\nfunc getRandomObjByQuotaBalanced(ctx context.Context, reqPath BalancedObjs, strict bool, objSize int64) (BalancedObjs, bool) {\n\t// Get all space\n\tdetails := make([]*model.StorageDetails, len(reqPath))\n\tdetailsChan := make(chan detailWithIndex, len(reqPath))\n\tworkerCount := 0\n\tfor i, p := range reqPath {\n\t\ts, err := fs.GetStorage(p.GetPath(), &fs.GetStoragesArgs{})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := s.(driver.WithDetails); !ok {\n\t\t\tcontinue\n\t\t}\n\t\tworkerCount++\n\t\tgo func(dri driver.Driver, i int) {\n\t\t\td, e := op.GetStorageDetails(ctx, dri)\n\t\t\tif e != nil {\n\t\t\t\tif !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {\n\t\t\t\t\tlog.Errorf(\"failed get %s storage details: %+v\", dri.GetStorage().MountPath, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdetailsChan <- detailWithIndex{idx: i, val: d}\n\t\t}(s, i)\n\t}\n\tfor workerCount > 0 {\n\t\tselect {\n\t\tcase r := <-detailsChan:\n\t\t\tdetails[r.idx] = r.val\n\t\t\tworkerCount--\n\t\tcase <-time.After(time.Second):\n\t\t\tworkerCount = 0\n\t\t}\n\t}\n\n\t// Try select one that has space info\n\tselected, ok := selectRandom(details, func(d *model.StorageDetails) uint64 {\n\t\tif d == nil || d.FreeSpace() < objSize {\n\t\t\treturn 0\n\t\t}\n\t\treturn uint64(d.FreeSpace())\n\t})\n\tif !ok {\n\t\tif strict {\n\t\t\treturn nil, false\n\t\t} else {\n\t\t\t// No strict mode, return any of non-details ones\n\t\t\tnoDetails := make([]int, 0, len(details))\n\t\t\tfor i, d := range details {\n\t\t\t\tif d == nil {\n\t\t\t\t\tnoDetails = append(noDetails, i)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(noDetails) == 0 {\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\tselected = noDetails[rand.Intn(len(noDetails))]\n\t\t}\n\t}\n\treturn reqPath[selected : selected+1], true\n}\n\nfunc selectRandom[Item any](arr []Item, getWeight func(Item) uint64) (int, bool) {\n\tvar totalWeight uint64 = 0\n\tfor _, i := range arr {\n\t\ttotalWeight += getWeight(i)\n\t}\n\tif totalWeight == 0 {\n\t\treturn 0, false\n\t}\n\tr := rand.Uint64() % totalWeight\n\tfor i, item := range arr {\n\t\tw := getWeight(item)\n\t\tif r < w {\n\t\t\treturn i, true\n\t\t}\n\t\tr -= w\n\t}\n\treturn 0, false\n}\n\nfunc (d *Alias) getCopyObjs(ctx context.Context, srcObj, dstDir model.Obj) (BalancedObjs, BalancedObjs, error) {\n\tif d.PutConflictPolicy == DisabledWP {\n\t\treturn nil, nil, errs.PermissionDenied\n\t}\n\tdstObjs, err := d.getAllObjs(ctx, dstDir, getWriteAndPutFilterFunc(d.PutConflictPolicy))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdstStorageMap := make(map[string][]model.Obj)\n\tallocatingDst := make(map[model.Obj]struct{})\n\tfor _, o := range dstObjs {\n\t\tstorage, e := fs.GetStorage(o.GetPath(), &fs.GetStoragesArgs{})\n\t\tif e != nil {\n\t\t\treturn nil, nil, errors.WithMessagef(e, \"cannot copy to virtual path [%s]\", o.GetPath())\n\t\t}\n\t\tmp := storage.GetStorage().MountPath\n\t\tdstStorageMap[mp] = append(dstStorageMap[mp], o)\n\t\tallocatingDst[o] = struct{}{}\n\t}\n\ttmpSrcObjs, err := d.getAllObjs(ctx, srcObj, getWriteAndPutFilterFunc(AllRWP))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tsrcObjs := make(BalancedObjs, 0, len(dstObjs))\n\tfor _, src := range tmpSrcObjs {\n\t\tstorage, e := fs.GetStorage(src.GetPath(), &fs.GetStoragesArgs{})\n\t\tif e != nil {\n\t\t\tcontinue\n\t\t}\n\t\tmp := storage.GetStorage().MountPath\n\t\tif tmp, ok := dstStorageMap[mp]; ok {\n\t\t\tfor _, dst := range tmp {\n\t\t\t\tdstObjs[len(srcObjs)] = dst\n\t\t\t\tsrcObjs = append(srcObjs, src)\n\t\t\t\tdelete(allocatingDst, dst)\n\t\t\t}\n\t\t\tdelete(dstStorageMap, mp)\n\t\t}\n\t}\n\tdstObjs = dstObjs[:len(srcObjs)]\n\tfor dst := range allocatingDst {\n\t\tsrc := tmpSrcObjs[0]\n\t\tif d.ReadConflictPolicy == RandomBalancedRP || d.ReadConflictPolicy == AllRWP {\n\t\t\tsrc = tmpSrcObjs[rand.Intn(len(tmpSrcObjs))]\n\t\t}\n\t\tsrcObjs = append(srcObjs, src)\n\t\tdstObjs = append(dstObjs, dst)\n\t}\n\treturn srcObjs, dstObjs, nil\n}\n\nfunc (d *Alias) getMoveObjs(ctx context.Context, srcObj, dstDir model.Obj) (BalancedObjs, BalancedObjs, error) {\n\tif d.PutConflictPolicy == DisabledWP {\n\t\treturn nil, nil, errs.PermissionDenied\n\t}\n\tdstObjs, err := d.getAllObjs(ctx, dstDir, getWriteAndPutFilterFunc(d.PutConflictPolicy))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttmpSrcObjs, err := d.getAllObjs(ctx, srcObj, getWriteAndPutFilterFunc(AllRWP))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif len(tmpSrcObjs) < len(dstObjs) {\n\t\treturn nil, nil, ErrNotEnoughSrcObjs\n\t}\n\tdstStorageMap := make(map[string][]model.Obj)\n\tallocatingDst := make(map[model.Obj]struct{})\n\tfor _, o := range dstObjs {\n\t\tstorage, e := fs.GetStorage(o.GetPath(), &fs.GetStoragesArgs{})\n\t\tif e != nil {\n\t\t\treturn nil, nil, errors.WithMessagef(e, \"cannot move to virtual path [%s]\", o.GetPath())\n\t\t}\n\t\tmp := storage.GetStorage().MountPath\n\t\tdstStorageMap[mp] = append(dstStorageMap[mp], o)\n\t\tallocatingDst[o] = struct{}{}\n\t}\n\tsrcObjs := make(BalancedObjs, 0, len(tmpSrcObjs))\n\trestSrcObjs := make(BalancedObjs, 0, len(tmpSrcObjs)-len(dstObjs))\n\tfor _, src := range tmpSrcObjs {\n\t\tstorage, e := fs.GetStorage(src.GetPath(), &fs.GetStoragesArgs{})\n\t\tif e != nil {\n\t\t\tcontinue\n\t\t}\n\t\tmp := storage.GetStorage().MountPath\n\t\tif tmp, ok := dstStorageMap[mp]; ok {\n\t\t\tdst := tmp[0]\n\t\t\tif len(tmp) == 1 {\n\t\t\t\tdelete(dstStorageMap, mp)\n\t\t\t} else {\n\t\t\t\tdstStorageMap[mp] = tmp[1:]\n\t\t\t}\n\t\t\tdstObjs[len(srcObjs)] = dst\n\t\t\tsrcObjs = append(srcObjs, src)\n\t\t\tdelete(allocatingDst, dst)\n\t\t} else {\n\t\t\trestSrcObjs = append(restSrcObjs, src)\n\t\t}\n\t}\n\tdstObjs = dstObjs[:len(srcObjs)]\n\t// len(restSrcObjs) >= len(allocatingDst)\n\tsrcObjs = append(srcObjs, restSrcObjs...)\n\tfor dst := range allocatingDst {\n\t\tdstObjs = append(dstObjs, dst)\n\t}\n\treturn srcObjs, dstObjs, nil\n}\n\nfunc (d *Alias) getArchiveMeta(ctx context.Context, reqPath string, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, ok := storage.(driver.ArchiveReader); ok {\n\t\treturn op.GetArchiveMeta(ctx, storage, reqActualPath, model.ArchiveMetaArgs{\n\t\t\tArchiveArgs: args,\n\t\t\tRefresh:     true,\n\t\t})\n\t}\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Alias) listArchive(ctx context.Context, reqPath string, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, ok := storage.(driver.ArchiveReader); ok {\n\t\treturn op.ListArchive(ctx, storage, reqActualPath, model.ArchiveListArgs{\n\t\t\tArchiveInnerArgs: args,\n\t\t\tRefresh:          true,\n\t\t})\n\t}\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Alias) extract(ctx context.Context, reqPath string, args model.ArchiveInnerArgs) (*model.Link, error) {\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, ok := storage.(driver.ArchiveReader); !ok {\n\t\treturn nil, errs.NotImplement\n\t}\n\tif args.Redirect && common.ShouldProxy(storage, stdpath.Base(reqPath)) {\n\t\t_, err := fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})\n\t\tif err == nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t}\n\tlink, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)\n\treturn link, err\n}\n\nfunc getAllSort(dirs []model.Obj) model.Sort {\n\tret := model.Sort{}\n\tnoSort := false\n\tnoExtractFolder := false\n\tfor _, dir := range dirs {\n\t\tif dir == nil {\n\t\t\tcontinue\n\t\t}\n\t\tstorage, err := fs.GetStorage(dir.GetPath(), &fs.GetStoragesArgs{})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif !noSort && storage.GetStorage().OrderBy != \"\" {\n\t\t\tif ret.OrderBy == \"\" {\n\t\t\t\tret.OrderBy = storage.GetStorage().OrderBy\n\t\t\t\tret.OrderDirection = storage.GetStorage().OrderDirection\n\t\t\t\tif ret.OrderDirection == \"\" {\n\t\t\t\t\tret.OrderDirection = \"asc\"\n\t\t\t\t}\n\t\t\t} else if ret.OrderBy != storage.GetStorage().OrderBy || ret.OrderDirection != storage.GetStorage().OrderDirection {\n\t\t\t\tret.OrderBy = \"\"\n\t\t\t\tret.OrderDirection = \"\"\n\t\t\t\tnoSort = true\n\t\t\t}\n\t\t}\n\t\tif !noExtractFolder && storage.GetStorage().ExtractFolder != \"\" {\n\t\t\tif ret.ExtractFolder == \"\" {\n\t\t\t\tret.ExtractFolder = storage.GetStorage().ExtractFolder\n\t\t\t} else if ret.ExtractFolder != storage.GetStorage().ExtractFolder {\n\t\t\t\tret.ExtractFolder = \"\"\n\t\t\t\tnoExtractFolder = true\n\t\t\t}\n\t\t}\n\t\tif noSort && noExtractFolder {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "drivers/alist_v3/driver.go",
    "content": "package alist_v3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AListV3 struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *AListV3) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AListV3) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AListV3) Init(ctx context.Context) error {\n\td.Addition.Address = strings.TrimSuffix(d.Addition.Address, \"/\")\n\tvar resp common.Resp[MeResp]\n\t_, _, err := d.request(\"/me\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\t// if the username is not empty and the username is not the same as the current username, then login again\n\tif d.Username != resp.Data.Username {\n\t\terr = d.login()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// re-get the user info\n\t_, _, err = d.request(\"/me\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif utils.SliceContains(resp.Data.Role, model.GUEST) {\n\t\tu := d.Address + \"/api/public/settings\"\n\t\tres, err := base.RestyClient.R().Get(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tallowMounted := utils.Json.Get(res.Body(), \"data\", conf.AllowMounted).ToString() == \"true\"\n\t\tif !allowMounted {\n\t\t\treturn fmt.Errorf(\"the site does not allow mounted\")\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *AListV3) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar resp common.Resp[FsListResp]\n\t_, _, err := d.request(\"/fs/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ListReq{\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tPath:     dir.GetPath(),\n\t\t\tPassword: d.MetaPassword,\n\t\t\tRefresh:  false,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp common.Resp[FsGetResp]\n\theaders := map[string]string{\n\t\t\"User-Agent\": base.UserAgent,\n\t}\n\t// if PassUAToUpsteam is true, then pass the user-agent to the upstream\n\tif d.PassUAToUpsteam {\n\t\tuserAgent := args.Header.Get(\"user-agent\")\n\t\tif userAgent != \"\" {\n\t\t\theaders[\"User-Agent\"] = userAgent\n\t\t}\n\t}\n\t// if PassIPToUpsteam is true, then pass the ip address to the upstream\n\tif d.PassIPToUpsteam {\n\t\tip := args.IP\n\t\tif ip != \"\" {\n\t\t\theaders[\"X-Forwarded-For\"] = ip\n\t\t\theaders[\"X-Real-Ip\"] = ip\n\t\t}\n\t}\n\t_, _, err := d.request(\"/fs/get\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(FsGetReq{\n\t\t\tPath:     file.GetPath(),\n\t\t\tPassword: d.MetaPassword,\n\t\t}).SetHeaders(headers)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: resp.Data.RawURL,\n\t}, nil\n}\n\nfunc (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, _, err := d.request(\"/fs/mkdir\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MkdirOrLinkReq{\n\t\t\tPath: path.Join(parentDir.GetPath(), dirName),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, _, err := d.request(\"/fs/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MoveCopyReq{\n\t\t\tSrcDir: path.Dir(srcObj.GetPath()),\n\t\t\tDstDir: dstDir.GetPath(),\n\t\t\tNames:  []string{srcObj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, _, err := d.request(\"/fs/rename\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(RenameReq{\n\t\t\tPath: srcObj.GetPath(),\n\t\t\tName: newName,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, _, err := d.request(\"/fs/copy\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MoveCopyReq{\n\t\t\tSrcDir: path.Dir(srcObj.GetPath()),\n\t\t\tDstDir: dstDir.GetPath(),\n\t\t\tNames:  []string{srcObj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {\n\t_, _, err := d.request(\"/fs/remove\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(RemoveReq{\n\t\t\tDir:   path.Dir(obj.GetPath()),\n\t\t\tNames: []string{obj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+\"/api/fs/put\", reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Authorization\", d.Token)\n\treq.Header.Set(\"File-Path\", path.Join(dstDir.GetPath(), s.GetName()))\n\treq.Header.Set(\"Password\", d.MetaPassword)\n\tif md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 {\n\t\treq.Header.Set(\"X-File-Md5\", md5)\n\t}\n\tif sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 {\n\t\treq.Header.Set(\"X-File-Sha1\", sha1)\n\t}\n\tif sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 {\n\t\treq.Header.Set(\"X-File-Sha256\", sha256)\n\t}\n\n\treq.ContentLength = s.GetSize()\n\t// client := base.NewHttpClient()\n\t// client.Timeout = time.Hour * 6\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbytes, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"[openlist] response body: %s\", string(bytes))\n\tif res.StatusCode >= 400 {\n\t\treturn fmt.Errorf(\"request failed, status: %s\", res.Status)\n\t}\n\tcode := utils.Json.Get(bytes, \"code\").ToInt()\n\tif code != 200 {\n\t\tif code == 401 || code == 403 {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"request failed,code: %d, message: %s\", code, utils.Json.Get(bytes, \"message\").ToString())\n\t}\n\treturn nil\n}\n\nfunc (d *AListV3) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveMetaResp]\n\t_, code, err := d.request(\"/fs/archive/meta\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveMetaReq{\n\t\t\tArchivePass: args.Password,\n\t\t\tPassword:    d.MetaPassword,\n\t\t\tPath:        obj.GetPath(),\n\t\t\tRefresh:     false,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar tree []model.ObjTree\n\tif resp.Data.Content != nil {\n\t\ttree = make([]model.ObjTree, 0, len(resp.Data.Content))\n\t\tfor _, content := range resp.Data.Content {\n\t\t\ttree = append(tree, &content)\n\t\t}\n\t}\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   resp.Data.Comment,\n\t\tEncrypted: resp.Data.Encrypted,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (d *AListV3) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveListResp]\n\t_, code, err := d.request(\"/fs/archive/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveListReq{\n\t\t\tArchiveMetaReq: ArchiveMetaReq{\n\t\t\t\tArchivePass: args.Password,\n\t\t\t\tPassword:    d.MetaPassword,\n\t\t\t\tPath:        obj.GetPath(),\n\t\t\t\tRefresh:     false,\n\t\t\t},\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tInnerPath: args.InnerPath,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *AListV3) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotSupport\n\t}\n\tvar resp common.Resp[ArchiveMetaResp]\n\t_, _, err := d.request(\"/fs/archive/meta\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveMetaReq{\n\t\t\tArchivePass: args.Password,\n\t\t\tPassword:    d.MetaPassword,\n\t\t\tPath:        obj.GetPath(),\n\t\t\tRefresh:     false,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: fmt.Sprintf(\"%s?inner=%s&pass=%s&sign=%s\",\n\t\t\tresp.Data.RawURL,\n\t\t\tutils.EncodePath(args.InnerPath, true),\n\t\t\turl.QueryEscape(args.Password),\n\t\t\tresp.Data.Sign),\n\t}, nil\n}\n\nfunc (d *AListV3) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {\n\tif !d.ForwardArchiveReq {\n\t\treturn errs.NotImplement\n\t}\n\tdir, name := path.Split(srcObj.GetPath())\n\t_, _, err := d.request(\"/fs/archive/decompress\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(DecompressReq{\n\t\t\tArchivePass:   args.Password,\n\t\t\tCacheFull:     args.CacheFull,\n\t\t\tDstDir:        dstDir.GetPath(),\n\t\t\tInnerPath:     args.InnerPath,\n\t\t\tName:          []string{name},\n\t\t\tPutIntoNewDir: args.PutIntoNewDir,\n\t\t\tSrcDir:        dir,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AListV3) ResolveLinkCacheMode(_ string) driver.LinkCacheMode {\n\tvar mode driver.LinkCacheMode\n\tif d.PassIPToUpsteam {\n\t\tmode |= driver.LinkCacheIP\n\t}\n\tif d.PassUAToUpsteam {\n\t\tmode |= driver.LinkCacheUA\n\t}\n\treturn mode\n}\n\nvar _ driver.Driver = (*AListV3)(nil)\n"
  },
  {
    "path": "drivers/alist_v3/meta.go",
    "content": "package alist_v3\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress           string `json:\"url\" required:\"true\"`\n\tMetaPassword      string `json:\"meta_password\"`\n\tUsername          string `json:\"username\"`\n\tPassword          string `json:\"password\"`\n\tToken             string `json:\"token\"`\n\tPassIPToUpsteam   bool   `json:\"pass_ip_to_upsteam\" default:\"true\"`\n\tPassUAToUpsteam   bool   `json:\"pass_ua_to_upsteam\" default:\"true\"`\n\tForwardArchiveReq bool   `json:\"forward_archive_requests\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:             \"AList V3\",\n\tLocalSort:        true,\n\tDefaultRoot:      \"/\",\n\tProxyRangeOption: true,\n\tLinkCacheMode:    driver.LinkCacheAuto,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AListV3{}\n\t})\n}\n"
  },
  {
    "path": "drivers/alist_v3/types.go",
    "content": "package alist_v3\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ListReq struct {\n\tmodel.PageReq\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n\tRefresh  bool   `json:\"refresh\"`\n}\n\ntype ObjResp struct {\n\tName     string    `json:\"name\"`\n\tSize     int64     `json:\"size\"`\n\tIsDir    bool      `json:\"is_dir\"`\n\tModified time.Time `json:\"modified\"`\n\tCreated  time.Time `json:\"created\"`\n\tSign     string    `json:\"sign\"`\n\tThumb    string    `json:\"thumb\"`\n\tType     int       `json:\"type\"`\n\tHashInfo string    `json:\"hashinfo\"`\n}\n\ntype FsListResp struct {\n\tContent  []ObjResp `json:\"content\"`\n\tTotal    int64     `json:\"total\"`\n\tReadme   string    `json:\"readme\"`\n\tWrite    bool      `json:\"write\"`\n\tProvider string    `json:\"provider\"`\n}\n\ntype FsGetReq struct {\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n}\n\ntype FsGetResp struct {\n\tObjResp\n\tRawURL   string    `json:\"raw_url\"`\n\tReadme   string    `json:\"readme\"`\n\tProvider string    `json:\"provider\"`\n\tRelated  []ObjResp `json:\"related\"`\n}\n\ntype MkdirOrLinkReq struct {\n\tPath string `json:\"path\" form:\"path\"`\n}\n\ntype MoveCopyReq struct {\n\tSrcDir string   `json:\"src_dir\"`\n\tDstDir string   `json:\"dst_dir\"`\n\tNames  []string `json:\"names\"`\n}\n\ntype RenameReq struct {\n\tPath string `json:\"path\"`\n\tName string `json:\"name\"`\n}\n\ntype RemoveReq struct {\n\tDir   string   `json:\"dir\"`\n\tNames []string `json:\"names\"`\n}\n\ntype LoginResp struct {\n\tToken string `json:\"token\"`\n}\n\ntype MeResp struct {\n\tId         int      `json:\"id\"`\n\tUsername   string   `json:\"username\"`\n\tPassword   string   `json:\"password\"`\n\tBasePath   string   `json:\"base_path\"`\n\tRole       IntSlice `json:\"role\"`\n\tDisabled   bool     `json:\"disabled\"`\n\tPermission int      `json:\"permission\"`\n\tSsoId      string   `json:\"sso_id\"`\n\tOtp        bool     `json:\"otp\"`\n}\n\ntype IntSlice []int\n\nfunc (s *IntSlice) UnmarshalJSON(b []byte) error {\n\tvar i int\n\tif json.Unmarshal(b, &i) == nil {\n\t\t*s = []int{i}\n\t\treturn nil\n\t}\n\treturn json.Unmarshal(b, (*[]int)(s))\n}\n\ntype ArchiveMetaReq struct {\n\tArchivePass string `json:\"archive_pass\"`\n\tPassword    string `json:\"password\"`\n\tPath        string `json:\"path\"`\n\tRefresh     bool   `json:\"refresh\"`\n}\n\ntype TreeResp struct {\n\tObjResp\n\tChildren  []TreeResp `json:\"children\"`\n\thashCache *utils.HashInfo\n}\n\nfunc (t *TreeResp) GetSize() int64 {\n\treturn t.Size\n}\n\nfunc (t *TreeResp) GetName() string {\n\treturn t.Name\n}\n\nfunc (t *TreeResp) ModTime() time.Time {\n\treturn t.Modified\n}\n\nfunc (t *TreeResp) CreateTime() time.Time {\n\treturn t.Created\n}\n\nfunc (t *TreeResp) IsDir() bool {\n\treturn t.ObjResp.IsDir\n}\n\nfunc (t *TreeResp) GetHash() utils.HashInfo {\n\treturn utils.FromString(t.HashInfo)\n}\n\nfunc (t *TreeResp) GetID() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetPath() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetChildren() []model.ObjTree {\n\tret := make([]model.ObjTree, 0, len(t.Children))\n\tfor _, child := range t.Children {\n\t\tret = append(ret, &child)\n\t}\n\treturn ret\n}\n\nfunc (t *TreeResp) Thumb() string {\n\treturn t.ObjResp.Thumb\n}\n\ntype ArchiveMetaResp struct {\n\tComment   string     `json:\"comment\"`\n\tEncrypted bool       `json:\"encrypted\"`\n\tContent   []TreeResp `json:\"content\"`\n\tRawURL    string     `json:\"raw_url\"`\n\tSign      string     `json:\"sign\"`\n}\n\ntype ArchiveListReq struct {\n\tmodel.PageReq\n\tArchiveMetaReq\n\tInnerPath string `json:\"inner_path\"`\n}\n\ntype ArchiveListResp struct {\n\tContent []ObjResp `json:\"content\"`\n\tTotal   int64     `json:\"total\"`\n}\n\ntype DecompressReq struct {\n\tArchivePass   string   `json:\"archive_pass\"`\n\tCacheFull     bool     `json:\"cache_full\"`\n\tDstDir        string   `json:\"dst_dir\"`\n\tInnerPath     string   `json:\"inner_path\"`\n\tName          []string `json:\"name\"`\n\tPutIntoNewDir bool     `json:\"put_into_new_dir\"`\n\tSrcDir        string   `json:\"src_dir\"`\n}\n"
  },
  {
    "path": "drivers/alist_v3/util.go",
    "content": "package alist_v3\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *AListV3) login() error {\n\tif d.Username == \"\" {\n\t\treturn nil\n\t}\n\tvar resp common.Resp[LoginResp]\n\t_, _, err := d.request(\"/auth/login\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(base.Json{\n\t\t\t\"username\": d.Username,\n\t\t\t\"password\": d.Password,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Token = resp.Data.Token\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) {\n\turl := d.Address + \"/api\" + api\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", d.Token)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\tcode := 0\n\t\tif res != nil {\n\t\t\tcode = res.StatusCode()\n\t\t}\n\t\treturn nil, code, err\n\t}\n\tlog.Debugf(\"[openlist] response body: %s\", res.String())\n\tif res.StatusCode() >= 400 {\n\t\treturn nil, res.StatusCode(), fmt.Errorf(\"request failed, status: %s\", res.Status())\n\t}\n\tcode := utils.Json.Get(res.Body(), \"code\").ToInt()\n\tif code != 200 {\n\t\tif (code == 401 || code == 403) && !utils.IsBool(retry...) {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, code, err\n\t\t\t}\n\t\t\treturn d.request(api, method, callback, true)\n\t\t}\n\t\treturn nil, code, fmt.Errorf(\"request failed,code: %d, message: %s\", code, utils.Json.Get(res.Body(), \"message\").ToString())\n\t}\n\treturn res.Body(), 200, nil\n}\n"
  },
  {
    "path": "drivers/aliyundrive/driver.go",
    "content": "package aliyundrive\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AliDrive struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n\tcron        *cron.Cron\n\tDriveId     string\n\tUserID      string\n}\n\nfunc (d *AliDrive) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AliDrive) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AliDrive) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t// op.MustSaveDriverStorage(d)\n\terr := d.refreshToken()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// get driver id\n\tres, err, _ := d.request(\"https://api.alipan.com/v2/user/get\", http.MethodPost, nil, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.DriveId = utils.Json.Get(res, \"default_drive_id\").ToString()\n\td.UserID = utils.Json.Get(res, \"user_id\").ToString()\n\td.cron = cron.NewCron(time.Hour * 2)\n\td.cron.Do(func() {\n\t\terr := d.refreshToken()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t}\n\t})\n\tif global.Has(d.UserID) {\n\t\treturn nil\n\t}\n\t// init deviceID\n\tdeviceID := utils.HashData(utils.SHA256, []byte(d.UserID))\n\t// init privateKey\n\tprivateKey, _ := NewPrivateKeyFromHex(deviceID)\n\tstate := State{\n\t\tprivateKey: privateKey,\n\t\tdeviceID:   deviceID,\n\t}\n\t// store state\n\tglobal.Store(d.UserID, &state)\n\t// init signature\n\td.sign()\n\treturn nil\n}\n\nfunc (d *AliDrive) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\treturn nil\n}\n\nfunc (d *AliDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdata := base.Json{\n\t\t\"drive_id\":   d.DriveId,\n\t\t\"file_id\":    file.GetID(),\n\t\t\"expire_sec\": 14400,\n\t}\n\tres, err, _ := d.request(\"https://api.alipan.com/v2/file/get_download_url\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tHeader: http.Header{\n\t\t\t\"Referer\": []string{\"https://www.alipan.com/\"},\n\t\t},\n\t\tURL: utils.Json.Get(res, \"url\").ToString(),\n\t}, nil\n}\n\nfunc (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err, _ := d.request(\"https://api.alipan.com/adrive/v2/file/createWithFolders\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"check_name_mode\": \"refuse\",\n\t\t\t\"drive_id\":        d.DriveId,\n\t\t\t\"name\":            dirName,\n\t\t\t\"parent_file_id\":  parentDir.GetID(),\n\t\t\t\"type\":            \"folder\",\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *AliDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\terr := d.batch(srcObj.GetID(), dstDir.GetID(), \"/file/move\")\n\treturn err\n}\n\nfunc (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err, _ := d.request(\"https://api.alipan.com/v3/file/update\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"check_name_mode\": \"refuse\",\n\t\t\t\"drive_id\":        d.DriveId,\n\t\t\t\"file_id\":         srcObj.GetID(),\n\t\t\t\"name\":            newName,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\terr := d.batch(srcObj.GetID(), dstDir.GetID(), \"/file/copy\")\n\treturn err\n}\n\nfunc (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err, _ := d.request(\"https://api.alipan.com/v2/recyclebin/trash\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\": d.DriveId,\n\t\t\t\"file_id\":  obj.GetID(),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {\n\tfile := &stream.FileStream{\n\t\tObj:      streamer,\n\t\tReader:   streamer,\n\t\tMimetype: streamer.GetMimetype(),\n\t}\n\tconst DEFAULT int64 = 10485760\n\tcount := int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))\n\n\tpartInfoList := make([]base.Json, 0, count)\n\tfor i := 1; i <= count; i++ {\n\t\tpartInfoList = append(partInfoList, base.Json{\"part_number\": i})\n\t}\n\treqBody := base.Json{\n\t\t\"check_name_mode\": \"overwrite\",\n\t\t\"drive_id\":        d.DriveId,\n\t\t\"name\":            file.GetName(),\n\t\t\"parent_file_id\":  dstDir.GetID(),\n\t\t\"part_info_list\":  partInfoList,\n\t\t\"size\":            file.GetSize(),\n\t\t\"type\":            \"file\",\n\t}\n\n\tvar localFile *os.File\n\tif fileStream, ok := file.Reader.(*stream.FileStream); ok {\n\t\tlocalFile, _ = fileStream.Reader.(*os.File)\n\t}\n\tif d.RapidUpload {\n\t\tbuf := bytes.NewBuffer(make([]byte, 0, 1024))\n\t\t_, err := utils.CopyWithBufferN(buf, file, 1024)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treqBody[\"pre_hash\"] = utils.HashData(utils.SHA1, buf.Bytes())\n\t\tif localFile != nil {\n\t\t\tif _, err := localFile.Seek(0, io.SeekStart); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// 把头部拼接回去\n\t\t\tfile.Reader = struct {\n\t\t\t\tio.Reader\n\t\t\t\tio.Closer\n\t\t\t}{\n\t\t\t\tReader: io.MultiReader(buf, file),\n\t\t\t\tCloser: file,\n\t\t\t}\n\t\t}\n\t} else {\n\t\treqBody[\"content_hash_name\"] = \"none\"\n\t\treqBody[\"proof_version\"] = \"v1\"\n\t}\n\n\tvar resp UploadResp\n\t_, err, e := d.request(\"https://api.alipan.com/adrive/v2/file/createWithFolders\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(reqBody)\n\t}, &resp)\n\n\tif err != nil && e.Code != \"PreHashMatched\" {\n\t\treturn err\n\t}\n\n\tif d.RapidUpload && e.Code == \"PreHashMatched\" {\n\t\tdelete(reqBody, \"pre_hash\")\n\t\th := sha1.New()\n\t\tif localFile != nil {\n\t\t\tif err = utils.CopyWithCtx(ctx, h, localFile, 0, nil); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, err = localFile.Seek(0, io.SeekStart); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\ttempFile, err := os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = tempFile.Close()\n\t\t\t\t_ = os.Remove(tempFile.Name())\n\t\t\t}()\n\t\t\tif err = utils.CopyWithCtx(ctx, io.MultiWriter(tempFile, h), file, 0, nil); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tlocalFile = tempFile\n\t\t}\n\t\treqBody[\"content_hash\"] = hex.EncodeToString(h.Sum(nil))\n\t\treqBody[\"content_hash_name\"] = \"sha1\"\n\t\treqBody[\"proof_version\"] = \"v1\"\n\n\t\t/*\n\t\t\tjs 隐性转换太坑不知道有没有bug\n\t\t\tvar n = e.access_token，\n\t\t\tr = new BigNumber('0x'.concat(md5(n).slice(0, 16)))，\n\t\t\ti = new BigNumber(t.file.size)，\n\t\t\to = i ? r.mod(i) : new gt.BigNumber(0);\n\t\t\t(t.file.slice(o.toNumber(), Math.min(o.plus(8).toNumber(), t.file.size)))\n\t\t*/\n\t\tbuf := make([]byte, 8)\n\t\tr, _ := new(big.Int).SetString(utils.GetMD5EncodeStr(d.AccessToken)[:16], 16)\n\t\ti := new(big.Int).SetInt64(file.GetSize())\n\t\to := new(big.Int).SetInt64(0)\n\t\tif file.GetSize() > 0 {\n\t\t\to = r.Mod(r, i)\n\t\t}\n\t\tn, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])\n\t\treqBody[\"proof_code\"] = base64.StdEncoding.EncodeToString(buf[:n])\n\n\t\t_, err, e := d.request(\"https://api.alipan.com/adrive/v2/file/createWithFolders\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(reqBody)\n\t\t}, &resp)\n\t\tif err != nil && e.Code != \"PreHashMatched\" {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RapidUpload {\n\t\t\treturn nil\n\t\t}\n\t\t// 秒传失败\n\t\tif _, err = localFile.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfile.Reader = localFile\n\t}\n\n\trateLimited := driver.NewLimitedUploadStream(ctx, file)\n\tfor i, partInfo := range resp.PartInfoList {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\turl := partInfo.UploadUrl\n\t\tif d.InternalUpload {\n\t\t\turl = partInfo.InternalUploadUrl\n\t\t}\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, io.LimitReader(rateLimited, DEFAULT))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_ = res.Body.Close()\n\t\tif count > 0 {\n\t\t\tup(float64(i) * 100 / float64(count))\n\t\t}\n\t}\n\tvar resp2 base.Json\n\t_, err, e = d.request(\"https://api.alipan.com/v2/file/complete\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":  d.DriveId,\n\t\t\t\"file_id\":   resp.FileId,\n\t\t\t\"upload_id\": resp.UploadId,\n\t\t})\n\t}, &resp2)\n\tif err != nil && e.Code != \"PreHashMatched\" {\n\t\treturn err\n\t}\n\tif resp2[\"file_id\"] == resp.FileId {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"%+v\", resp2)\n}\n\nfunc (d *AliDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tres, err, _ := d.request(\"https://api.aliyundrive.com/adrive/v1/user/driveCapacityDetails\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused := utils.Json.Get(res, \"drive_used_size\").ToInt64()\n\ttotal := utils.Json.Get(res, \"drive_total_size\").ToInt64()\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nfunc (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tvar resp base.Json\n\tvar url string\n\tdata := base.Json{\n\t\t\"drive_id\": d.DriveId,\n\t\t\"file_id\":  args.Obj.GetID(),\n\t}\n\tswitch args.Method {\n\tcase \"doc_preview\":\n\t\turl = \"https://api.alipan.com/v2/file/get_office_preview_url\"\n\t\tdata[\"access_token\"] = d.AccessToken\n\tcase \"video_preview\":\n\t\turl = \"https://api.alipan.com/v2/file/get_video_preview_play_info\"\n\t\tdata[\"category\"] = \"live_transcoding\"\n\t\tdata[\"url_expire_sec\"] = 14400\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\t_, err, _ := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nvar _ driver.Driver = (*AliDrive)(nil)\n"
  },
  {
    "path": "drivers/aliyundrive/global.go",
    "content": "package aliyundrive\n\nimport (\n\t\"crypto/ecdsa\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync\"\n)\n\ntype State struct {\n\tdeviceID   string\n\tsignature  string\n\tretry      int\n\tprivateKey *ecdsa.PrivateKey\n}\n\nvar global = generic_sync.MapOf[string, *State]{}\n"
  },
  {
    "path": "drivers/aliyundrive/help.go",
    "content": "package aliyundrive\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"math/big\"\n\n\t\"github.com/dustinxie/ecc\"\n)\n\nfunc NewPrivateKey() (*ecdsa.PrivateKey, error) {\n\tp256k1 := ecc.P256k1()\n\treturn ecdsa.GenerateKey(p256k1, rand.Reader)\n}\n\nfunc NewPrivateKeyFromHex(hex_ string) (*ecdsa.PrivateKey, error) {\n\tdata, err := hex.DecodeString(hex_)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewPrivateKeyFromBytes(data), nil\n\n}\n\nfunc NewPrivateKeyFromBytes(priv []byte) *ecdsa.PrivateKey {\n\tp256k1 := ecc.P256k1()\n\tx, y := p256k1.ScalarBaseMult(priv)\n\treturn &ecdsa.PrivateKey{\n\t\tPublicKey: ecdsa.PublicKey{\n\t\t\tCurve: p256k1,\n\t\t\tX:     x,\n\t\t\tY:     y,\n\t\t},\n\t\tD: new(big.Int).SetBytes(priv),\n\t}\n}\n\nfunc PrivateKeyToHex(private *ecdsa.PrivateKey) string {\n\treturn hex.EncodeToString(PrivateKeyToBytes(private))\n}\n\nfunc PrivateKeyToBytes(private *ecdsa.PrivateKey) []byte {\n\treturn private.D.Bytes()\n}\n\nfunc PublicKeyToHex(public *ecdsa.PublicKey) string {\n\treturn hex.EncodeToString(PublicKeyToBytes(public))\n}\n\nfunc PublicKeyToBytes(public *ecdsa.PublicKey) []byte {\n\tx := public.X.Bytes()\n\tif len(x) < 32 {\n\t\tfor i := 0; i < 32-len(x); i++ {\n\t\t\tx = append([]byte{0}, x...)\n\t\t}\n\t}\n\n\ty := public.Y.Bytes()\n\tif len(y) < 32 {\n\t\tfor i := 0; i < 32-len(y); i++ {\n\t\t\ty = append([]byte{0}, y...)\n\t\t}\n\t}\n\treturn append(x, y...)\n}\n"
  },
  {
    "path": "drivers/aliyundrive/meta.go",
    "content": "package aliyundrive\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\n\t//DeviceID       string `json:\"device_id\" required:\"true\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"name,size,updated_at,created_at\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"ASC,DESC\"`\n\tRapidUpload    bool   `json:\"rapid_upload\"`\n\tInternalUpload bool   `json:\"internal_upload\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Aliyundrive\",\n\tDefaultRoot: \"root\",\n\tAlert: `warning|There may be an infinite loop bug in this driver.\nDeprecated, no longer maintained and will be removed in a future version.\nWe recommend using the official driver AliyundriveOpen.`,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AliDrive{}\n\t})\n}\n"
  },
  {
    "path": "drivers/aliyundrive/types.go",
    "content": "package aliyundrive\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype RespErr struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Files struct {\n\tItems      []File `json:\"items\"`\n\tNextMarker string `json:\"next_marker\"`\n}\n\ntype File struct {\n\tDriveId       string     `json:\"drive_id\"`\n\tCreatedAt     *time.Time `json:\"created_at\"`\n\tFileExtension string     `json:\"file_extension\"`\n\tFileId        string     `json:\"file_id\"`\n\tType          string     `json:\"type\"`\n\tName          string     `json:\"name\"`\n\tCategory      string     `json:\"category\"`\n\tParentFileId  string     `json:\"parent_file_id\"`\n\tUpdatedAt     time.Time  `json:\"updated_at\"`\n\tSize          int64      `json:\"size\"`\n\tThumbnail     string     `json:\"thumbnail\"`\n\tUrl           string     `json:\"url\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.FileId,\n\t\t\tName:     f.Name,\n\t\t\tSize:     f.Size,\n\t\t\tModified: f.UpdatedAt,\n\t\t\tIsFolder: f.Type == \"folder\",\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},\n\t}\n}\n\ntype UploadResp struct {\n\tFileId       string `json:\"file_id\"`\n\tUploadId     string `json:\"upload_id\"`\n\tPartInfoList []struct {\n\t\tUploadUrl         string `json:\"upload_url\"`\n\t\tInternalUploadUrl string `json:\"internal_upload_url\"`\n\t} `json:\"part_info_list\"`\n\n\tRapidUpload bool `json:\"rapid_upload\"`\n}\n"
  },
  {
    "path": "drivers/aliyundrive/util.go",
    "content": "package aliyundrive\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/dustinxie/ecc\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n)\n\nfunc (d *AliDrive) createSession() error {\n\tstate, ok := global.Load(d.UserID)\n\tif !ok {\n\t\treturn fmt.Errorf(\"can't load user state, user_id: %s\", d.UserID)\n\t}\n\td.sign()\n\tstate.retry++\n\tif state.retry > 3 {\n\t\tstate.retry = 0\n\t\treturn fmt.Errorf(\"createSession failed after three retries\")\n\t}\n\t_, err, _ := d.request(\"https://api.alipan.com/users/v1/users/device/create_session\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"deviceName\":   \"samsung\",\n\t\t\t\"modelName\":    \"SM-G9810\",\n\t\t\t\"nonce\":        0,\n\t\t\t\"pubKey\":       PublicKeyToHex(&state.privateKey.PublicKey),\n\t\t\t\"refreshToken\": d.RefreshToken,\n\t\t})\n\t}, nil)\n\tif err == nil {\n\t\tstate.retry = 0\n\t}\n\treturn err\n}\n\n// func (d *AliDrive) renewSession() error {\n// \t_, err, _ := d.request(\"https://api.alipan.com/users/v1/users/device/renew_session\", http.MethodPost, nil, nil)\n// \treturn err\n// }\n\nfunc (d *AliDrive) sign() {\n\tstate, _ := global.Load(d.UserID)\n\tsecpAppID := \"5dde4e1bdf9e4966b387ba58f4b3fdc3\"\n\tsingdata := fmt.Sprintf(\"%s:%s:%s:%d\", secpAppID, state.deviceID, d.UserID, 0)\n\thash := sha256.Sum256([]byte(singdata))\n\tdata, _ := ecc.SignBytes(state.privateKey, hash[:], ecc.RecID|ecc.LowerS)\n\tstate.signature = hex.EncodeToString(data) //strconv.Itoa(state.nonce)\n}\n\n// do others that not defined in Driver interface\n\nfunc (d *AliDrive) refreshToken() error {\n\turl := \"https://auth.alipan.com/v2/account/token\"\n\tvar resp base.TokenResp\n\tvar e RespErr\n\t_, err := base.RestyClient.R().\n\t\t//ForceContentType(\"application/json\").\n\t\tSetBody(base.Json{\"refresh_token\": d.RefreshToken, \"grant_type\": \"refresh_token\"}).\n\t\tSetResult(&resp).\n\t\tSetError(&e).\n\t\tPost(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Code != \"\" {\n\t\treturn fmt.Errorf(\"failed to refresh token: %s\", e.Message)\n\t}\n\tif resp.RefreshToken == \"\" {\n\t\treturn errors.New(\"failed to refresh token: refresh token is empty\")\n\t}\n\td.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *AliDrive) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error, RespErr) {\n\treq := base.RestyClient.R()\n\tstate, ok := global.Load(d.UserID)\n\tif !ok {\n\t\tif url == \"https://api.alipan.com/v2/user/get\" {\n\t\t\tstate = &State{}\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"can't load user state, user_id: %s\", d.UserID), RespErr{}\n\t\t}\n\t}\n\treq.SetHeaders(map[string]string{\n\t\t\"Authorization\": \"Bearer\\t\" + d.AccessToken,\n\t\t\"content-type\":  \"application/json\",\n\t\t\"origin\":        \"https://www.alipan.com\",\n\t\t\"Referer\":       \"https://alipan.com/\",\n\t\t\"X-Signature\":   state.signature,\n\t\t\"x-request-id\":  uuid.NewString(),\n\t\t\"X-Canary\":      \"client=Android,app=adrive,version=v4.1.0\",\n\t\t\"X-Device-Id\":   state.deviceID,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t} else {\n\t\treq.SetBody(\"{}\")\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e RespErr\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err, e\n\t}\n\tif e.Code != \"\" {\n\t\tswitch e.Code {\n\t\tcase \"AccessTokenInvalid\":\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err, e\n\t\t\t}\n\t\tcase \"DeviceSessionSignatureInvalid\":\n\t\t\terr = d.createSession()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err, e\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errors.New(e.Message), e\n\t\t}\n\t\treturn d.request(url, method, callback, resp)\n\t} else if res.IsError() {\n\t\treturn nil, errors.New(\"bad status code \" + res.Status()), e\n\t}\n\treturn res.Body(), nil, e\n}\n\nfunc (d *AliDrive) getFiles(fileId string) ([]File, error) {\n\tmarker := \"first\"\n\tres := make([]File, 0)\n\tfor marker != \"\" {\n\t\tif marker == \"first\" {\n\t\t\tmarker = \"\"\n\t\t}\n\t\tvar resp Files\n\t\tdata := base.Json{\n\t\t\t\"drive_id\":                d.DriveId,\n\t\t\t\"fields\":                  \"*\",\n\t\t\t\"image_thumbnail_process\": \"image/resize,w_400/format,jpeg\",\n\t\t\t\"image_url_process\":       \"image/resize,w_1920/format,jpeg\",\n\t\t\t\"limit\":                   200,\n\t\t\t\"marker\":                  marker,\n\t\t\t\"order_by\":                d.OrderBy,\n\t\t\t\"order_direction\":         d.OrderDirection,\n\t\t\t\"parent_file_id\":          fileId,\n\t\t\t\"video_thumbnail_process\": \"video/snapshot,t_0,f_jpg,ar_auto,w_300\",\n\t\t\t\"url_expire_sec\":          14400,\n\t\t}\n\t\t_, err, _ := d.request(\"https://api.alipan.com/v2/file/list\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(data)\n\t\t}, &resp)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmarker = resp.NextMarker\n\t\tres = append(res, resp.Items...)\n\t}\n\treturn res, nil\n}\n\nfunc (d *AliDrive) batch(srcId, dstId string, url string) error {\n\tres, err, _ := d.request(\"https://api.alipan.com/v3/batch\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"requests\": []base.Json{\n\t\t\t\t{\n\t\t\t\t\t\"headers\": base.Json{\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\"id\":     srcId,\n\t\t\t\t\t\"body\": base.Json{\n\t\t\t\t\t\t\"drive_id\":          d.DriveId,\n\t\t\t\t\t\t\"file_id\":           srcId,\n\t\t\t\t\t\t\"to_drive_id\":       d.DriveId,\n\t\t\t\t\t\t\"to_parent_file_id\": dstId,\n\t\t\t\t\t},\n\t\t\t\t\t\"url\": url,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"resource\": \"file\",\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstatus := utils.Json.Get(res, \"responses\", 0, \"status\").ToInt()\n\tif status < 400 && status >= 100 {\n\t\treturn nil\n\t}\n\treturn errors.New(string(res))\n}\n"
  },
  {
    "path": "drivers/aliyundrive_open/driver.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AliyundriveOpen struct {\n\tmodel.Storage\n\tAddition\n\n\tDriveId string\n\n\tlimiter *limiter\n\tref     *AliyundriveOpen\n}\n\nfunc (d *AliyundriveOpen) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AliyundriveOpen) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AliyundriveOpen) Init(ctx context.Context) error {\n\td.limiter = getLimiterForUser(globalLimiterUserID) // First create a globally shared limiter to limit the initial requests.\n\tif d.LIVPDownloadFormat == \"\" {\n\t\td.LIVPDownloadFormat = \"jpeg\"\n\t}\n\tif d.DriveType == \"\" {\n\t\td.DriveType = \"default\"\n\t}\n\tres, err := d.request(ctx, limiterOther, \"/adrive/v1.0/user/getDriveInfo\", http.MethodPost, nil)\n\tif err != nil {\n\t\td.limiter.free()\n\t\td.limiter = nil\n\t\treturn err\n\t}\n\td.DriveId = utils.Json.Get(res, d.DriveType+\"_drive_id\").ToString()\n\tuserid := utils.Json.Get(res, \"user_id\").ToString()\n\td.limiter.free()\n\td.limiter = getLimiterForUser(userid) // Allocate a corresponding limiter for each user.\n\treturn nil\n}\n\nfunc (d *AliyundriveOpen) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*AliyundriveOpen)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *AliyundriveOpen) Drop(ctx context.Context) error {\n\td.limiter.free()\n\td.limiter = nil\n\td.ref = nil\n\treturn nil\n}\n\n// GetRoot implements the driver.GetRooter interface to properly set up the root object\nfunc (d *AliyundriveOpen) GetRoot(ctx context.Context) (model.Obj, error) {\n\treturn &model.Object{\n\t\tID:       d.RootFolderID,\n\t\tPath:     \"/\",\n\t\tName:     \"root\",\n\t\tModified: d.Modified,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tobjs, err := utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\tobj := fileToObj(src)\n\t\t// Set the correct path for the object\n\t\tif dir.GetPath() != \"\" {\n\t\t\tobj.Path = filepath.Join(dir.GetPath(), obj.GetName())\n\t\t}\n\t\treturn obj, nil\n\t})\n\n\treturn objs, err\n}\n\nfunc (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tres, err := d.request(ctx, limiterLink, \"/adrive/v1.0/openFile/getDownloadUrl\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":   d.DriveId,\n\t\t\t\"file_id\":    file.GetID(),\n\t\t\t\"expire_sec\": 14400,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\turl := utils.Json.Get(res, \"url\").ToString()\n\tif url == \"\" {\n\t\tif utils.Ext(file.GetName()) != \"livp\" {\n\t\t\treturn nil, errors.New(\"get download url failed: \" + string(res))\n\t\t}\n\t\turl = utils.Json.Get(res, \"streamsUrl\", d.LIVPDownloadFormat).ToString()\n\t}\n\texp := time.Minute\n\treturn &model.Link{\n\t\tURL:        url,\n\t\tExpiration: &exp,\n\t}, nil\n}\n\nfunc (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tnowTime, _ := getNowTime()\n\tnewDir := File{CreatedAt: nowTime, UpdatedAt: nowTime}\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/create\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":        d.DriveId,\n\t\t\t\"parent_file_id\":  parentDir.GetID(),\n\t\t\t\"name\":            dirName,\n\t\t\t\"type\":            \"folder\",\n\t\t\t\"check_name_mode\": \"refuse\",\n\t\t}).SetResult(&newDir)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobj := fileToObj(newDir)\n\n\t// Set the correct Path for the returned directory object\n\tif parentDir.GetPath() != \"\" {\n\t\tobj.Path = filepath.Join(parentDir.GetPath(), dirName)\n\t} else {\n\t\tobj.Path = \"/\" + dirName\n\t}\n\n\treturn obj, nil\n}\n\nfunc (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar resp MoveOrCopyResp\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":          d.DriveId,\n\t\t\t\"file_id\":           srcObj.GetID(),\n\t\t\t\"to_parent_file_id\": dstDir.GetID(),\n\t\t\t\"check_name_mode\":   \"ignore\", // optional:ignore,auto_rename,refuse\n\t\t\t//\"new_name\":          \"newName\", // The new name to use when a file of the same name exists\n\t\t}).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif srcObj, ok := srcObj.(*model.ObjThumb); ok {\n\t\tsrcObj.ID = resp.FileID\n\t\tsrcObj.Modified = time.Now()\n\t\tsrcObj.Path = filepath.Join(dstDir.GetPath(), srcObj.GetName())\n\n\t\t// Check for duplicate files in the destination directory\n\t\tif err := d.removeDuplicateFiles(ctx, dstDir.GetPath(), srcObj.GetName(), srcObj.GetID()); err != nil {\n\t\t\t// Only log a warning instead of returning an error since the move operation has already completed successfully\n\t\t\tlog.Warnf(\"Failed to remove duplicate files after move: %v\", err)\n\t\t}\n\t\treturn srcObj, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tvar newFile File\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/update\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\": d.DriveId,\n\t\t\t\"file_id\":  srcObj.GetID(),\n\t\t\t\"name\":     newName,\n\t\t}).SetResult(&newFile)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Check for duplicate files in the parent directory\n\tparentPath := filepath.Dir(srcObj.GetPath())\n\tif err := d.removeDuplicateFiles(ctx, parentPath, newName, newFile.FileId); err != nil {\n\t\t// Only log a warning instead of returning an error since the rename operation has already completed successfully\n\t\tlog.Warnf(\"Failed to remove duplicate files after rename: %v\", err)\n\t}\n\n\tobj := fileToObj(newFile)\n\n\t// Set the correct Path for the renamed object\n\tif parentPath != \"\" && parentPath != \".\" {\n\t\tobj.Path = filepath.Join(parentPath, newName)\n\t} else {\n\t\tobj.Path = \"/\" + newName\n\t}\n\n\treturn obj, nil\n}\n\nfunc (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tvar resp MoveOrCopyResp\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/copy\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":          d.DriveId,\n\t\t\t\"file_id\":           srcObj.GetID(),\n\t\t\t\"to_parent_file_id\": dstDir.GetID(),\n\t\t\t\"auto_rename\":       false,\n\t\t}).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check for duplicate files in the destination directory\n\tif err := d.removeDuplicateFiles(ctx, dstDir.GetPath(), srcObj.GetName(), resp.FileID); err != nil {\n\t\t// Only log a warning instead of returning an error since the copy operation has already completed successfully\n\t\tlog.Warnf(\"Failed to remove duplicate files after copy: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {\n\turi := \"/adrive/v1.0/openFile/recyclebin/trash\"\n\tif d.RemoveWay == \"delete\" {\n\t\turi = \"/adrive/v1.0/openFile/delete\"\n\t}\n\t_, err := d.request(ctx, limiterOther, uri, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\": d.DriveId,\n\t\t\t\"file_id\":  obj.GetID(),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tobj, err := d.upload(ctx, dstDir, stream, up)\n\n\t// Set the correct Path for the returned file object\n\tif obj != nil && obj.GetPath() == \"\" {\n\t\tif dstDir.GetPath() != \"\" {\n\t\t\tif objWithPath, ok := obj.(model.SetPath); ok {\n\t\t\t\tobjWithPath.SetPath(filepath.Join(dstDir.GetPath(), obj.GetName()))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn obj, err\n}\n\nfunc (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tvar resp base.Json\n\tvar uri string\n\tdata := base.Json{\n\t\t\"drive_id\": d.DriveId,\n\t\t\"file_id\":  args.Obj.GetID(),\n\t}\n\tswitch args.Method {\n\tcase \"video_preview\":\n\t\turi = \"/adrive/v1.0/openFile/getVideoPreviewPlayInfo\"\n\t\tdata[\"category\"] = \"live_transcoding\"\n\t\tdata[\"url_expire_sec\"] = 14400\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\t_, err := d.request(ctx, limiterOther, uri, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (d *AliyundriveOpen) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tres, err := d.request(ctx, limiterOther, \"/adrive/v1.0/user/getSpaceInfo\", http.MethodPost, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal := utils.Json.Get(res, \"personal_space_info\", \"total_size\").ToInt64()\n\tused := utils.Json.Get(res, \"personal_space_info\", \"used_size\").ToInt64()\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*AliyundriveOpen)(nil)\nvar _ driver.MkdirResult = (*AliyundriveOpen)(nil)\nvar _ driver.MoveResult = (*AliyundriveOpen)(nil)\nvar _ driver.RenameResult = (*AliyundriveOpen)(nil)\nvar _ driver.PutResult = (*AliyundriveOpen)(nil)\nvar _ driver.GetRooter = (*AliyundriveOpen)(nil)\n"
  },
  {
    "path": "drivers/aliyundrive_open/limiter.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"golang.org/x/time/rate\"\n)\n\n// See document https://www.yuque.com/aliyundrive/zpfszx/mqocg38hlxzc5vcd\n// See issue https://github.com/OpenListTeam/OpenList/issues/724\n// We got limit per user per app, so the limiter should be global.\n\ntype limiterType int\n\nconst (\n\tlimiterList limiterType = iota\n\tlimiterLink\n\tlimiterOther\n)\n\nconst (\n\tlistRateLimit       = 3.9  // 4 per second in document, but we use 3.9 per second to be safe\n\tlinkRateLimit       = 0.9  // 1 per second in document, but we use 0.9 per second to be safe\n\totherRateLimit      = 14.9 // 15 per second in document, but we use 14.9 per second to be safe\n\tglobalLimiterUserID = \"\"   // Global limiter user ID, used to limit the initial requests.\n)\n\ntype limiter struct {\n\tusedBy int\n\tlist   *rate.Limiter\n\tlink   *rate.Limiter\n\tother  *rate.Limiter\n}\n\nvar limiters = make(map[string]*limiter)\nvar limitersLock = &sync.Mutex{}\n\nfunc getLimiterForUser(userid string) *limiter {\n\tlimitersLock.Lock()\n\tdefer limitersLock.Unlock()\n\tdefer func() {\n\t\t// Clean up limiters that are no longer used.\n\t\tfor id, lim := range limiters {\n\t\t\tif lim.usedBy <= 0 && id != globalLimiterUserID { // Do not delete the global limiter.\n\t\t\t\tdelete(limiters, id)\n\t\t\t}\n\t\t}\n\t}()\n\tif lim, ok := limiters[userid]; ok {\n\t\tlim.usedBy++\n\t\treturn lim\n\t}\n\tlim := &limiter{\n\t\tusedBy: 1,\n\t\tlist:   rate.NewLimiter(rate.Limit(listRateLimit), 1),\n\t\tlink:   rate.NewLimiter(rate.Limit(linkRateLimit), 1),\n\t\tother:  rate.NewLimiter(rate.Limit(otherRateLimit), 1),\n\t}\n\tlimiters[userid] = lim\n\treturn lim\n}\n\nfunc (l *limiter) wait(ctx context.Context, typ limiterType) error {\n\tif l == nil {\n\t\treturn fmt.Errorf(\"driver not init\")\n\t}\n\tswitch typ {\n\tcase limiterList:\n\t\treturn l.list.Wait(ctx)\n\tcase limiterLink:\n\t\treturn l.link.Wait(ctx)\n\tcase limiterOther:\n\t\treturn l.other.Wait(ctx)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown limiter type\")\n\t}\n}\nfunc (l *limiter) free() {\n\tif l == nil {\n\t\treturn\n\t}\n\tlimitersLock.Lock()\n\tdefer limitersLock.Unlock()\n\tl.usedBy--\n}\nfunc (d *AliyundriveOpen) wait(ctx context.Context, typ limiterType) error {\n\tif d == nil {\n\t\treturn fmt.Errorf(\"driver not init\")\n\t}\n\tif d.ref != nil {\n\t\treturn d.ref.wait(ctx, typ) // If this is a reference driver, wait on the reference driver.\n\t}\n\treturn d.limiter.wait(ctx, typ)\n}\n"
  },
  {
    "path": "drivers/aliyundrive_open/meta.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tDriveType string `json:\"drive_type\" type:\"select\" options:\"default,resource,backup\" default:\"resource\"`\n\tdriver.RootID\n\tRefreshToken       string `json:\"refresh_token\" required:\"true\"`\n\tOrderBy            string `json:\"order_by\" type:\"select\" options:\"name,size,updated_at,created_at\"`\n\tOrderDirection     string `json:\"order_direction\" type:\"select\" options:\"ASC,DESC\"`\n\tUseOnlineAPI       bool   `json:\"use_online_api\" default:\"true\"`\n\tAlipanType         string `json:\"alipan_type\" required:\"true\" type:\"select\" default:\"default\" options:\"default,alipanTV\"`\n\tAPIAddress         string `json:\"api_url_address\" default:\"https://api.oplist.org/alicloud/renewapi\"`\n\tClientID           string `json:\"client_id\" help:\"Keep it empty if you don't have one\"`\n\tClientSecret       string `json:\"client_secret\" help:\"Keep it empty if you don't have one\"`\n\tRemoveWay          string `json:\"remove_way\" required:\"true\" type:\"select\" options:\"trash,delete\"`\n\tRapidUpload        bool   `json:\"rapid_upload\" help:\"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect\"`\n\tInternalUpload     bool   `json:\"internal_upload\" help:\"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed\"`\n\tLIVPDownloadFormat string `json:\"livp_download_format\" type:\"select\" options:\"jpeg,mov\" default:\"jpeg\"`\n\tAccessToken        string\n}\n\nvar config = driver.Config{\n\tName:              \"AliyundriveOpen\",\n\tDefaultRoot:       \"root\",\n\tNoOverwriteUpload: true,\n}\nvar API_URL = \"https://openapi.alipan.com\"\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AliyundriveOpen{}\n\t})\n}\n"
  },
  {
    "path": "drivers/aliyundrive_open/types.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype ErrResp struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Files struct {\n\tItems      []File `json:\"items\"`\n\tNextMarker string `json:\"next_marker\"`\n}\n\ntype File struct {\n\tDriveId       string    `json:\"drive_id\"`\n\tFileId        string    `json:\"file_id\"`\n\tParentFileId  string    `json:\"parent_file_id\"`\n\tName          string    `json:\"name\"`\n\tSize          int64     `json:\"size\"`\n\tFileExtension string    `json:\"file_extension\"`\n\tContentHash   string    `json:\"content_hash\"`\n\tCategory      string    `json:\"category\"`\n\tType          string    `json:\"type\"`\n\tThumbnail     string    `json:\"thumbnail\"`\n\tUrl           string    `json:\"url\"`\n\tCreatedAt     time.Time `json:\"created_at\"`\n\tUpdatedAt     time.Time `json:\"updated_at\"`\n\n\t// create only\n\tFileName string `json:\"file_name\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\tif f.Name == \"\" {\n\t\tf.Name = f.FileName\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.FileId,\n\t\t\tName:     f.Name,\n\t\t\tSize:     f.Size,\n\t\t\tModified: f.UpdatedAt,\n\t\t\tIsFolder: f.Type == \"folder\",\n\t\t\tCtime:    f.CreatedAt,\n\t\t\tHashInfo: utils.NewHashInfo(utils.SHA1, f.ContentHash),\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},\n\t}\n}\n\ntype PartInfo struct {\n\tEtag        interface{} `json:\"etag\"`\n\tPartNumber  int         `json:\"part_number\"`\n\tPartSize    interface{} `json:\"part_size\"`\n\tUploadUrl   string      `json:\"upload_url\"`\n\tContentType string      `json:\"content_type\"`\n}\n\ntype CreateResp struct {\n\t//Type         string `json:\"type\"`\n\t//ParentFileId string `json:\"parent_file_id\"`\n\t//DriveId      string `json:\"drive_id\"`\n\tFileId string `json:\"file_id\"`\n\t//RevisionId   string `json:\"revision_id\"`\n\t//EncryptMode  string `json:\"encrypt_mode\"`\n\t//DomainId     string `json:\"domain_id\"`\n\t//FileName     string `json:\"file_name\"`\n\tUploadId string `json:\"upload_id\"`\n\t//Location     string `json:\"location\"`\n\tRapidUpload  bool       `json:\"rapid_upload\"`\n\tPartInfoList []PartInfo `json:\"part_info_list\"`\n}\n\ntype MoveOrCopyResp struct {\n\tExist   bool   `json:\"exist\"`\n\tDriveID string `json:\"drive_id\"`\n\tFileID  string `json:\"file_id\"`\n}\n"
  },
  {
    "path": "drivers/aliyundrive_open/upload.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc makePartInfos(size int) []base.Json {\n\tpartInfoList := make([]base.Json, size)\n\tfor i := 0; i < size; i++ {\n\t\tpartInfoList[i] = base.Json{\"part_number\": 1 + i}\n\t}\n\treturn partInfoList\n}\n\nfunc calPartSize(fileSize int64) int64 {\n\tvar partSize int64 = 20 * utils.MB\n\tif fileSize > partSize {\n\t\tif fileSize > 1*utils.TB { // file Size over 1TB\n\t\t\tpartSize = 5 * utils.GB // file part size 5GB\n\t\t} else if fileSize > 768*utils.GB { // over 768GB\n\t\t\tpartSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part\n\t\t} else if fileSize > 512*utils.GB { // over 512GB\n\t\t\tpartSize = 82463373 // ≈ 78.6432MB\n\t\t} else if fileSize > 384*utils.GB { // over 384GB\n\t\t\tpartSize = 54975582 // ≈ 52.4288MB\n\t\t} else if fileSize > 256*utils.GB { // over 256GB\n\t\t\tpartSize = 41231687 // ≈ 39.3216MB\n\t\t} else if fileSize > 128*utils.GB { // over 128GB\n\t\t\tpartSize = 27487791 // ≈ 26.2144MB\n\t\t}\n\t}\n\treturn partSize\n}\n\nfunc (d *AliyundriveOpen) getUploadUrl(ctx context.Context, count int, fileId, uploadId string) ([]PartInfo, error) {\n\tpartInfoList := makePartInfos(count)\n\tvar resp CreateResp\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/getUploadUrl\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":       d.DriveId,\n\t\t\t\"file_id\":        fileId,\n\t\t\t\"part_info_list\": partInfoList,\n\t\t\t\"upload_id\":      uploadId,\n\t\t}).SetResult(&resp)\n\t})\n\treturn resp.PartInfoList, err\n}\n\nfunc (d *AliyundriveOpen) uploadPart(ctx context.Context, r io.Reader, partInfo PartInfo) error {\n\tuploadUrl := partInfo.UploadUrl\n\tif d.InternalUpload {\n\t\tuploadUrl = strings.ReplaceAll(uploadUrl, \"https://cn-beijing-data.aliyundrive.net/\", \"http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/\")\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = res.Body.Close()\n\tif res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {\n\t\treturn fmt.Errorf(\"upload status: %d\", res.StatusCode)\n\t}\n\treturn nil\n}\n\nfunc (d *AliyundriveOpen) completeUpload(ctx context.Context, fileId, uploadId string) (model.Obj, error) {\n\t// 3. complete\n\tvar newFile File\n\t_, err := d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/complete\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"drive_id\":  d.DriveId,\n\t\t\t\"file_id\":   fileId,\n\t\t\t\"upload_id\": uploadId,\n\t\t}).SetResult(&newFile)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fileToObj(newFile), nil\n}\n\ntype ProofRange struct {\n\tStart int64\n\tEnd   int64\n}\n\nfunc getProofRange(input string, size int64) (*ProofRange, error) {\n\tif size == 0 {\n\t\treturn &ProofRange{}, nil\n\t}\n\ttmpStr := utils.GetMD5EncodeStr(input)[0:16]\n\ttmpInt, err := strconv.ParseUint(tmpStr, 16, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tindex := tmpInt % uint64(size)\n\tpr := &ProofRange{\n\t\tStart: int64(index),\n\t\tEnd:   int64(index) + 8,\n\t}\n\tif pr.End >= size {\n\t\tpr.End = size\n\t}\n\treturn pr, nil\n}\n\nfunc (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error) {\n\tproofRange, err := getProofRange(d.getAccessToken(), stream.GetSize())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlength := proofRange.End - proofRange.Start\n\treader, err := stream.RangeRead(http_range.Range{Start: proofRange.Start, Length: length})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuf := make([]byte, length)\n\tn, err := io.ReadFull(reader, buf)\n\tif n != int(length) {\n\t\treturn \"\", fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\treturn base64.StdEncoding.EncodeToString(buf), nil\n}\n\nfunc (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// 1. create\n\t// Part Size Unit: Bytes, Default: 20MB,\n\t// Maximum number of slices 10,000, ≈195.3125GB\n\tvar partSize = calPartSize(stream.GetSize())\n\tconst dateFormat = \"2006-01-02T15:04:05.000Z\"\n\tmtimeStr := stream.ModTime().UTC().Format(dateFormat)\n\tctimeStr := stream.CreateTime().UTC().Format(dateFormat)\n\n\tcreateData := base.Json{\n\t\t\"drive_id\":          d.DriveId,\n\t\t\"parent_file_id\":    dstDir.GetID(),\n\t\t\"name\":              stream.GetName(),\n\t\t\"type\":              \"file\",\n\t\t\"check_name_mode\":   \"ignore\",\n\t\t\"local_modified_at\": mtimeStr,\n\t\t\"local_created_at\":  ctimeStr,\n\t}\n\tcount := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))\n\tcreateData[\"part_info_list\"] = makePartInfos(count)\n\t// rapid upload\n\trapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload\n\tif rapidUpload {\n\t\tlog.Debugf(\"[aliyundrive_open] start cal pre_hash\")\n\t\t// read 1024 bytes to calculate pre hash\n\t\treader, err := stream.RangeRead(http_range.Range{Start: 0, Length: 1024})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\thash, err := utils.HashReader(utils.SHA1, reader)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcreateData[\"size\"] = stream.GetSize()\n\t\tcreateData[\"pre_hash\"] = hash\n\t}\n\tvar createResp CreateResp\n\t_, err, e := d.requestReturnErrResp(ctx, limiterOther, \"/adrive/v1.0/openFile/create\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(createData).SetResult(&createResp)\n\t})\n\tif err != nil {\n\t\tif e.Code != \"PreHashMatched\" || !rapidUpload {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debugf(\"[aliyundrive_open] pre_hash matched, start rapid upload\")\n\n\t\thash := stream.GetHash().GetHash(utils.SHA1)\n\t\tif len(hash) != utils.SHA1.Width {\n\t\t\t_, hash, err = streamPkg.CacheFullAndHash(stream, &up, utils.SHA1)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tdelete(createData, \"pre_hash\")\n\t\tcreateData[\"proof_version\"] = \"v1\"\n\t\tcreateData[\"content_hash_name\"] = \"sha1\"\n\t\tcreateData[\"content_hash\"] = hash\n\t\tcreateData[\"proof_code\"], err = d.calProofCode(stream)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cal proof code error: %s\", err.Error())\n\t\t}\n\t\t_, err = d.request(ctx, limiterOther, \"/adrive/v1.0/openFile/create\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(createData).SetResult(&createResp)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif !createResp.RapidUpload {\n\t\t// 2. normal upload\n\t\tlog.Debugf(\"[aliyundive_open] normal upload\")\n\t\tss, err := streamPkg.NewStreamSectionReader(stream, int(partSize), &up)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpreTime := time.Now()\n\t\tvar offset, length int64 = 0, partSize\n\t\tfor i := 0; i < len(createResp.PartInfoList); i++ {\n\t\t\tif utils.IsCanceled(ctx) {\n\t\t\t\treturn nil, ctx.Err()\n\t\t\t}\n\t\t\t// refresh upload url if 50 minutes passed\n\t\t\tif time.Since(preTime) > 50*time.Minute {\n\t\t\t\tcreateResp.PartInfoList, err = d.getUploadUrl(ctx, count, createResp.FileId, createResp.UploadId)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tpreTime = time.Now()\n\t\t\t}\n\t\t\tif remain := stream.GetSize() - offset; length > remain {\n\t\t\t\tlength = remain\n\t\t\t}\n\t\t\trd, err := ss.GetSectionReader(offset, length)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\terr = retry.Do(func() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treturn d.uploadPart(ctx, driver.NewLimitedUploadStream(ctx, rd), createResp.PartInfoList[i])\n\t\t\t},\n\t\t\t\tretry.Context(ctx),\n\t\t\t\tretry.Attempts(3),\n\t\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\t\tretry.Delay(time.Second))\n\t\t\tss.FreeSectionReader(rd)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\toffset += partSize\n\t\t\tup(float64(i*100) / float64(count))\n\t\t}\n\t} else {\n\t\tlog.Debugf(\"[aliyundrive_open] rapid upload success, file id: %s\", createResp.FileId)\n\t}\n\n\tlog.Debugf(\"[aliyundrive_open] create file success, resp: %+v\", createResp)\n\t// 3. complete\n\treturn d.completeUpload(ctx, createResp.FileId, createResp.UploadId)\n}\n"
  },
  {
    "path": "drivers/aliyundrive_open/util.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *AliyundriveOpen) _refreshToken(ctx context.Context) (string, string, error) {\n\tif d.UseOnlineAPI && d.APIAddress != \"\" {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\n\t\t// 根据AlipanType选项设置driver_txt\n\t\tdriverTxt := \"alicloud_qr\"\n\t\tif d.AlipanType == \"alipanTV\" {\n\t\t\tdriverTxt = \"alicloud_tv\"\n\t\t}\n\t\terr := d.wait(ctx, limiterOther)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\t_, err = base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": driverTxt,\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn \"\", \"\", fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\treturn resp.RefreshToken, resp.AccessToken, nil\n\t}\n\t// 本地刷新逻辑，必须要求 client_id 和 client_secret\n\tif d.ClientID == \"\" || d.ClientSecret == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"empty ClientID or ClientSecret\")\n\t}\n\terr := d.wait(ctx, limiterOther)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\turl := API_URL + \"/oauth/access_token\"\n\t//var resp base.TokenResp\n\tvar e ErrResp\n\tres, err := base.RestyClient.R().\n\t\t//ForceContentType(\"application/json\").\n\t\tSetBody(base.Json{\n\t\t\t\"client_id\":     d.ClientID,\n\t\t\t\"client_secret\": d.ClientSecret,\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t}).\n\t\t//SetResult(&resp).\n\t\tSetError(&e).\n\t\tPost(url)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tlog.Debugf(\"[ali_open] refresh token response: %s\", res.String())\n\tif e.Code != \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: %s\", e.Message)\n\t}\n\trefresh, access := utils.Json.Get(res.Body(), \"refresh_token\").ToString(), utils.Json.Get(res.Body(), \"access_token\").ToString()\n\tif refresh == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: refresh token is empty, resp: %s\", res.String())\n\t}\n\tcurSub, err := getSub(d.RefreshToken)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tnewSub, err := getSub(refresh)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tif curSub != newSub {\n\t\treturn \"\", \"\", errors.New(\"failed to refresh token: sub not match\")\n\t}\n\treturn refresh, access, nil\n}\n\nfunc getSub(token string) (string, error) {\n\tsegments := strings.Split(token, \".\")\n\tif len(segments) != 3 {\n\t\treturn \"\", errors.New(\"not a jwt token because of invalid segments\")\n\t}\n\tbs, err := base64.RawStdEncoding.DecodeString(segments[1])\n\tif err != nil {\n\t\treturn \"\", errors.New(\"failed to decode jwt token\")\n\t}\n\treturn utils.Json.Get(bs, \"sub\").ToString(), nil\n}\n\nfunc (d *AliyundriveOpen) refreshToken(ctx context.Context) error {\n\tif d.ref != nil {\n\t\treturn d.ref.refreshToken(ctx)\n\t}\n\trefresh, access, err := d._refreshToken(ctx)\n\tfor i := 0; i < 3; i++ {\n\t\tif err == nil {\n\t\t\tbreak\n\t\t} else {\n\t\t\tlog.Errorf(\"[ali_open] failed to refresh token: %s\", err)\n\t\t}\n\t\trefresh, access, err = d._refreshToken(ctx)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Infof(\"[ali_open] token exchange: %s -> %s\", d.RefreshToken, refresh)\n\td.RefreshToken, d.AccessToken = refresh, access\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *AliyundriveOpen) request(ctx context.Context, limitTy limiterType, uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {\n\tb, err, _ := d.requestReturnErrResp(ctx, limitTy, uri, method, callback, retry...)\n\treturn b, err\n}\n\nfunc (d *AliyundriveOpen) requestReturnErrResp(ctx context.Context, limitTy limiterType, uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error, *ErrResp) {\n\treq := base.RestyClient.R()\n\t// TODO check whether access_token is expired\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.getAccessToken())\n\tif method == http.MethodPost {\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t}\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\terr := d.wait(ctx, limitTy)\n\tif err != nil {\n\t\treturn nil, err, nil\n\t}\n\tres, err := req.Execute(method, API_URL+uri)\n\tif err != nil {\n\t\tif res != nil {\n\t\t\tlog.Errorf(\"[aliyundrive_open] request error: %s\", res.String())\n\t\t}\n\t\treturn nil, err, nil\n\t}\n\tisRetry := len(retry) > 0 && retry[0]\n\tif e.Code != \"\" {\n\t\tif !isRetry && (utils.SliceContains([]string{\"AccessTokenInvalid\", \"AccessTokenExpired\", \"I400JD\"}, e.Code) || d.getAccessToken() == \"\") {\n\t\t\terr = d.refreshToken(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err, nil\n\t\t\t}\n\t\t\treturn d.requestReturnErrResp(ctx, limitTy, uri, method, callback, true)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%s:%s\", e.Code, e.Message), &e\n\t}\n\treturn res.Body(), nil, nil\n}\n\nfunc (d *AliyundriveOpen) list(ctx context.Context, data base.Json) (*Files, error) {\n\tvar resp Files\n\t_, err := d.request(ctx, limiterList, \"/adrive/v1.0/openFile/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *AliyundriveOpen) getFiles(ctx context.Context, fileId string) ([]File, error) {\n\tmarker := \"first\"\n\tres := make([]File, 0)\n\tfor marker != \"\" {\n\t\tif marker == \"first\" {\n\t\t\tmarker = \"\"\n\t\t}\n\t\tdata := base.Json{\n\t\t\t\"drive_id\":        d.DriveId,\n\t\t\t\"limit\":           200,\n\t\t\t\"marker\":          marker,\n\t\t\t\"order_by\":        d.OrderBy,\n\t\t\t\"order_direction\": d.OrderDirection,\n\t\t\t\"parent_file_id\":  fileId,\n\t\t\t//\"category\":              \"\",\n\t\t\t//\"type\":                  \"\",\n\t\t\t//\"video_thumbnail_time\":  120000,\n\t\t\t//\"video_thumbnail_width\": 480,\n\t\t\t//\"image_thumbnail_width\": 480,\n\t\t}\n\t\tresp, err := d.list(ctx, data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmarker = resp.NextMarker\n\t\tres = append(res, resp.Items...)\n\t}\n\treturn res, nil\n}\n\nfunc getNowTime() (time.Time, string) {\n\tnowTime := time.Now()\n\tnowTimeStr := nowTime.Format(\"2006-01-02T15:04:05.000Z\")\n\treturn nowTime, nowTimeStr\n}\n\nfunc (d *AliyundriveOpen) getAccessToken() string {\n\tif d.ref != nil {\n\t\treturn d.ref.getAccessToken()\n\t}\n\treturn d.AccessToken\n}\n\n// Remove duplicate files with the same name in the given directory path,\n// preserving the file with the given skipID if provided\nfunc (d *AliyundriveOpen) removeDuplicateFiles(ctx context.Context, parentPath string, fileName string, skipID string) error {\n\t// Handle empty path (root directory) case\n\tif parentPath == \"\" {\n\t\tparentPath = \"/\"\n\t}\n\n\t// List all files in the parent directory\n\tfiles, err := op.List(ctx, d, parentPath, model.ListArgs{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Find all files with the same name\n\tvar duplicates []model.Obj\n\tfor _, file := range files {\n\t\tif file.GetName() == fileName && file.GetID() != skipID {\n\t\t\tduplicates = append(duplicates, file)\n\t\t}\n\t}\n\n\t// Remove all duplicates files, except the file with the given ID\n\tfor _, file := range duplicates {\n\t\terr := d.Remove(ctx, file)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/aliyundrive_share/driver.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AliyundriveShare struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n\tShareToken  string\n\tDriveId     string\n\tcron        *cron.Cron\n\n\tlimiter *limiter\n}\n\nfunc (d *AliyundriveShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AliyundriveShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AliyundriveShare) Init(ctx context.Context) error {\n\td.limiter = getLimiter()\n\terr := d.refreshToken(ctx)\n\tif err != nil {\n\t\td.limiter.free()\n\t\td.limiter = nil\n\t\treturn err\n\t}\n\terr = d.getShareToken(ctx)\n\tif err != nil {\n\t\td.limiter.free()\n\t\td.limiter = nil\n\t\treturn err\n\t}\n\td.cron = cron.NewCron(time.Hour * 2)\n\td.cron.Do(func() {\n\t\terr := d.refreshToken(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t}\n\t})\n\treturn nil\n}\n\nfunc (d *AliyundriveShare) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\td.limiter.free()\n\td.limiter = nil\n\td.DriveId = \"\"\n\treturn nil\n}\n\nfunc (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdata := base.Json{\n\t\t\"drive_id\": d.DriveId,\n\t\t\"file_id\":  file.GetID(),\n\t\t// // Only ten minutes lifetime\n\t\t\"expire_sec\": 600,\n\t\t\"share_id\":   d.ShareId,\n\t}\n\tvar resp ShareLinkResp\n\t_, err := d.request(ctx, limiterLink, \"https://api.alipan.com/v2/file/get_share_link_download_url\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tHeader: http.Header{\n\t\t\t\"Referer\": []string{\"https://www.alipan.com/\"},\n\t\t},\n\t\tURL: resp.DownloadUrl,\n\t}, nil\n}\n\nfunc (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tvar resp base.Json\n\tvar url string\n\tdata := base.Json{\n\t\t\"share_id\": d.ShareId,\n\t\t\"file_id\":  args.Obj.GetID(),\n\t}\n\tswitch args.Method {\n\tcase \"doc_preview\":\n\t\turl = \"https://api.alipan.com/v2/file/get_office_preview_url\"\n\tcase \"video_preview\":\n\t\turl = \"https://api.alipan.com/v2/file/get_video_preview_play_info\"\n\t\tdata[\"category\"] = \"live_transcoding\"\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\t_, err := d.request(ctx, limiterOther, url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nvar _ driver.Driver = (*AliyundriveShare)(nil)\n"
  },
  {
    "path": "drivers/aliyundrive_share/limiter.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"golang.org/x/time/rate\"\n)\n\n// See issue https://github.com/OpenListTeam/OpenList/issues/724\n// Seems there is no limit per user.\n\ntype limiterType int\n\nconst (\n\tlimiterList limiterType = iota\n\tlimiterLink\n\tlimiterOther\n)\n\nconst (\n\tlistRateLimit  = 3.9  // 4 per second in document, but we use 3.9 per second to be safe\n\tlinkRateLimit  = 0.9  // 1 per second in document, but we use 0.9 per second to be safe\n\totherRateLimit = 14.9 // 15 per second in document, but we use 14.9 per second to be safe\n)\n\ntype limiter struct {\n\tlist  *rate.Limiter\n\tlink  *rate.Limiter\n\tother *rate.Limiter\n}\n\nfunc getLimiter() *limiter {\n\treturn &limiter{\n\t\tlist:  rate.NewLimiter(rate.Limit(listRateLimit), 1),\n\t\tlink:  rate.NewLimiter(rate.Limit(linkRateLimit), 1),\n\t\tother: rate.NewLimiter(rate.Limit(otherRateLimit), 1),\n\t}\n}\n\nfunc (l *limiter) wait(ctx context.Context, typ limiterType) error {\n\tif l == nil {\n\t\treturn fmt.Errorf(\"driver not init\")\n\t}\n\tswitch typ {\n\tcase limiterList:\n\t\treturn l.list.Wait(ctx)\n\tcase limiterLink:\n\t\treturn l.link.Wait(ctx)\n\tcase limiterOther:\n\t\treturn l.other.Wait(ctx)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown limiter type\")\n\t}\n}\nfunc (l *limiter) free() {\n\n}\nfunc (d *AliyundriveShare) wait(ctx context.Context, typ limiterType) error {\n\tif d == nil {\n\t\treturn fmt.Errorf(\"driver not init\")\n\t}\n\t//if d.ref != nil {\n\t//\treturn d.ref.wait(ctx, typ) // If this is a reference driver, wait on the reference driver.\n\t//}\n\treturn d.limiter.wait(ctx, typ)\n}\n"
  },
  {
    "path": "drivers/aliyundrive_share/meta.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\n\tShareId      string `json:\"share_id\" required:\"true\"`\n\tSharePwd     string `json:\"share_pwd\"`\n\tdriver.RootID\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"name,size,updated_at,created_at\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"ASC,DESC\"`\n}\n\nvar config = driver.Config{\n\tName:        \"AliyundriveShare\",\n\tLocalSort:   false,\n\tOnlyProxy:   false,\n\tNoUpload:    true,\n\tDefaultRoot: \"root\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AliyundriveShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/aliyundrive_share/types.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype ErrorResp struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype ShareTokenResp struct {\n\tShareToken string    `json:\"share_token\"`\n\tExpireTime time.Time `json:\"expire_time\"`\n\tExpiresIn  int       `json:\"expires_in\"`\n}\n\ntype ListResp struct {\n\tItems             []File `json:\"items\"`\n\tNextMarker        string `json:\"next_marker\"`\n\tPunishedFileCount int    `json:\"punished_file_count\"`\n}\n\ntype File struct {\n\tDriveId      string    `json:\"drive_id\"`\n\tDomainId     string    `json:\"domain_id\"`\n\tFileId       string    `json:\"file_id\"`\n\tShareId      string    `json:\"share_id\"`\n\tName         string    `json:\"name\"`\n\tType         string    `json:\"type\"`\n\tCreatedAt    time.Time `json:\"created_at\"`\n\tUpdatedAt    time.Time `json:\"updated_at\"`\n\tParentFileId string    `json:\"parent_file_id\"`\n\tSize         int64     `json:\"size\"`\n\tThumbnail    string    `json:\"thumbnail\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.FileId,\n\t\t\tName:     f.Name,\n\t\t\tSize:     f.Size,\n\t\t\tModified: f.UpdatedAt,\n\t\t\tCtime:    f.CreatedAt,\n\t\t\tIsFolder: f.Type == \"folder\",\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},\n\t}\n}\n\ntype ShareLinkResp struct {\n\tDownloadUrl string `json:\"download_url\"`\n\tUrl         string `json:\"url\"`\n\tThumbnail   string `json:\"thumbnail\"`\n}\n"
  },
  {
    "path": "drivers/aliyundrive_share/util.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\t// CanaryHeaderKey CanaryHeaderValue for lifting rate limit restrictions\n\tCanaryHeaderKey   = \"X-Canary\"\n\tCanaryHeaderValue = \"client=web,app=share,version=v2.3.1\"\n)\n\nfunc (d *AliyundriveShare) refreshToken(ctx context.Context) error {\n\terr := d.wait(ctx, limiterOther)\n\tif err != nil {\n\t\treturn err\n\t}\n\turl := \"https://auth.alipan.com/v2/account/token\"\n\tvar resp base.TokenResp\n\tvar e ErrorResp\n\t_, err = base.RestyClient.R().\n\t\tSetBody(base.Json{\"refresh_token\": d.RefreshToken, \"grant_type\": \"refresh_token\"}).\n\t\tSetResult(&resp).\n\t\tSetError(&e).\n\t\tPost(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Code != \"\" {\n\t\treturn fmt.Errorf(\"failed to refresh token: %s\", e.Message)\n\t}\n\td.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\n// do others that not defined in Driver interface\nfunc (d *AliyundriveShare) getShareToken(ctx context.Context) error {\n\terr := d.wait(ctx, limiterOther)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata := base.Json{\n\t\t\"share_id\": d.ShareId,\n\t}\n\tif d.SharePwd != \"\" {\n\t\tdata[\"share_pwd\"] = d.SharePwd\n\t}\n\tvar e ErrorResp\n\tvar resp ShareTokenResp\n\t_, err = base.RestyClient.R().\n\t\tSetResult(&resp).SetError(&e).SetBody(data).\n\t\tPost(\"https://api.alipan.com/v2/share_link/get_share_token\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Code != \"\" {\n\t\treturn errors.New(e.Message)\n\t}\n\td.ShareToken = resp.ShareToken\n\treturn nil\n}\n\nfunc (d *AliyundriveShare) request(ctx context.Context, limitTy limiterType, url, method string, callback base.ReqCallback) ([]byte, error) {\n\tvar e ErrorResp\n\treq := base.RestyClient.R().\n\t\tSetError(&e).\n\t\tSetHeader(\"content-type\", \"application/json\").\n\t\tSetHeader(\"Authorization\", \"Bearer\\t\"+d.AccessToken).\n\t\tSetHeader(CanaryHeaderKey, CanaryHeaderValue).\n\t\tSetHeader(\"x-share-token\", d.ShareToken)\n\tif callback != nil {\n\t\tcallback(req)\n\t} else {\n\t\treq.SetBody(\"{}\")\n\t}\n\terr := d.wait(ctx, limitTy)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Code != \"\" {\n\t\tif e.Code == \"AccessTokenInvalid\" || e.Code == \"ShareLinkTokenInvalid\" {\n\t\t\tif e.Code == \"AccessTokenInvalid\" {\n\t\t\t\terr = d.refreshToken(ctx)\n\t\t\t} else {\n\t\t\t\terr = d.getShareToken(ctx)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(ctx, limitTy, url, method, callback)\n\t\t} else {\n\t\t\treturn nil, errors.New(e.Code + \": \" + e.Message)\n\t\t}\n\t}\n\treturn resp.Body(), nil\n}\n\nfunc (d *AliyundriveShare) getFiles(ctx context.Context, fileId string) ([]File, error) {\n\tfiles := make([]File, 0)\n\tdata := base.Json{\n\t\t\"image_thumbnail_process\": \"image/resize,w_160/format,jpeg\",\n\t\t\"image_url_process\":       \"image/resize,w_1920/format,jpeg\",\n\t\t\"limit\":                   200,\n\t\t\"order_by\":                d.OrderBy,\n\t\t\"order_direction\":         d.OrderDirection,\n\t\t\"parent_file_id\":          fileId,\n\t\t\"share_id\":                d.ShareId,\n\t\t\"video_thumbnail_process\": \"video/snapshot,t_1000,f_jpg,ar_auto,w_300\",\n\t\t\"marker\":                  \"first\",\n\t}\n\tfor data[\"marker\"] != \"\" {\n\t\tif data[\"marker\"] == \"first\" {\n\t\t\tdata[\"marker\"] = \"\"\n\t\t}\n\t\terr := d.wait(ctx, limiterList)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar e ErrorResp\n\t\tvar resp ListResp\n\t\tres, err := base.RestyClient.R().\n\t\t\tSetHeader(\"x-share-token\", d.ShareToken).\n\t\t\tSetHeader(CanaryHeaderKey, CanaryHeaderValue).\n\t\t\tSetResult(&resp).SetError(&e).SetBody(data).\n\t\t\tPost(\"https://api.alipan.com/adrive/v3/file/list\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debugf(\"aliyundrive share get files: %s\", res.String())\n\t\tif e.Code != \"\" {\n\t\t\tif e.Code == \"AccessTokenInvalid\" || e.Code == \"ShareLinkTokenInvalid\" {\n\t\t\t\terr = d.getShareToken(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn d.getFiles(ctx, fileId)\n\t\t\t}\n\t\t\treturn nil, errors.New(e.Message)\n\t\t}\n\t\tdata[\"marker\"] = resp.NextMarker\n\t\tfiles = append(files, resp.Items...)\n\t}\n\tif len(files) > 0 && d.DriveId == \"\" {\n\t\td.DriveId = files[0].DriveId\n\t}\n\treturn files, nil\n}\n"
  },
  {
    "path": "drivers/all.go",
    "content": "package drivers\n\nimport (\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/115\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/115_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/115_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/123\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/123_link\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/123_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/123_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/139\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/189\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/189_tv\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/189pc\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/alias\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/alist_v3\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/aliyundrive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/aliyundrive_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/aliyundrive_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/autoindex\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/azure_blob\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/baidu_netdisk\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/baidu_photo\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/chaoxing\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/chunk\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/cloudreve\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/cnb_releases\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/crypt\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/degoo\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/doubao\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/doubao_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/dropbox\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/febbox\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/ftp\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/github\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/github_releases\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/google_drive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/google_photo\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/halalcloud\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/halalcloud_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/ilanzou\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/ipfs_api\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/kodbox\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/lanzou\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/lenovonas_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/local\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/mediafire\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/mediatrack\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/mega\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/misskey\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/mopan\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/netease_music\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/onedrive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/onedrive_app\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/onedrive_sharelink\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/openlist\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/openlist_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/pikpak\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/proton_drive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/quark_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/quark_uc\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/s3\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/seafile\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/sftp\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/smb\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/strm\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/teambition\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/teldrive\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/terabox\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/thunder\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/thunderx\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/url_tree\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/uss\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/virtual\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/webdav\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/weiyun\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/wopan\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/wps\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers/yandex_disk\"\n)\n\n// All do nothing,just for import\n// same as _ import\nfunc All() {\n}\n"
  },
  {
    "path": "drivers/autoindex/driver.go",
    "content": "package autoindex\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/antchfx/htmlquery\"\n\t\"github.com/antchfx/xpath\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype AutoIndex struct {\n\tmodel.Storage\n\tAddition\n\titemXPath     *xpath.Expr\n\tnameXPath     *xpath.Expr\n\tmodifiedXPath *xpath.Expr\n\tsizeXPath     *xpath.Expr\n\tignores       map[string]any\n}\n\nfunc (d *AutoIndex) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AutoIndex) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AutoIndex) Init(ctx context.Context) error {\n\tvar err error\n\td.itemXPath, err = xpath.Compile(d.ItemXPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to compile Item XPath\")\n\t}\n\td.nameXPath, err = xpath.Compile(d.NameXPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to compile Name XPath\")\n\t}\n\tif len(d.ModifiedXPath) > 0 {\n\t\td.modifiedXPath, err = xpath.Compile(d.ModifiedXPath)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed to compile Modified XPath\")\n\t\t}\n\t}\n\tif len(d.SizeXPath) > 0 {\n\t\td.sizeXPath, err = xpath.Compile(d.SizeXPath)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed to compile Size XPath\")\n\t\t}\n\t}\n\tignores := strings.Split(d.IgnoreFileNames, \"\\n\")\n\td.ignores = make(map[string]any, len(ignores))\n\tfor _, i := range ignores {\n\t\ti = strings.TrimSpace(i)\n\t\tif len(i) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\td.ignores[i] = struct{}{}\n\t}\n\thasScheme := strings.Contains(d.URL, \"://\")\n\thasSuffix := strings.HasSuffix(d.URL, \"/\")\n\tif !hasScheme || !hasSuffix {\n\t\tif !hasSuffix {\n\t\t\td.URL = d.URL + \"/\"\n\t\t}\n\t\tif !hasScheme {\n\t\t\td.URL = \"https://\" + d.URL\n\t\t}\n\t\top.MustSaveDriverStorage(d)\n\t}\n\treturn nil\n}\n\nfunc (d *AutoIndex) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *AutoIndex) GetRoot(ctx context.Context) (model.Obj, error) {\n\treturn &model.Object{\n\t\tName:     op.RootName,\n\t\tPath:     d.URL,\n\t\tModified: d.Modified,\n\t\tMask:     model.Locked,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (d *AutoIndex) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tres, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetDoNotParseResponse(true).\n\t\tGet(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to get url [%s]\", dir.GetPath())\n\t}\n\tdefer res.RawResponse.Body.Close()\n\tdoc, err := htmlquery.Parse(res.RawBody())\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to parse [%s]\", dir.GetPath())\n\t}\n\titemsIter := d.itemXPath.Select(htmlquery.CreateXPathNavigator(doc))\n\tvar objs []model.Obj\n\tfor itemsIter.MoveNext() {\n\t\tnameFull, err := parseString(d.nameXPath.Evaluate(itemsIter.Current().Copy()))\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"skip invalid name evaluating result: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tnameFull = strings.TrimSpace(nameFull)\n\t\tname, isDir := strings.CutSuffix(nameFull, \"/\")\n\t\tif _, ok := d.ignores[name]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tvar size int64 = 0\n\t\texact := false\n\t\tmodified := time.Now()\n\t\tif d.sizeXPath != nil {\n\t\t\tsize, exact, err = parseSize(d.sizeXPath.Evaluate(itemsIter.Current().Copy()))\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to parse size of %s: %v\", name, err)\n\t\t\t}\n\t\t}\n\t\tif d.modifiedXPath != nil {\n\t\t\tmodified, err = parseTime(d.modifiedXPath.Evaluate(itemsIter.Current().Copy()), d.ModifiedTimeFormat)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to parse modified time of %s: %v\", name, err)\n\t\t\t}\n\t\t}\n\t\tvar o model.Obj = &model.Object{\n\t\t\tName:     name,\n\t\t\tIsFolder: isDir,\n\t\t\tPath:     dir.GetPath() + nameFull,\n\t\t\tModified: modified,\n\t\t\tSize:     size,\n\t\t}\n\t\tif exact {\n\t\t\to = &exactSizeObj{Obj: o}\n\t\t}\n\t\tobjs = append(objs, o)\n\t}\n\treturn objs, nil\n}\n\nfunc (d *AutoIndex) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif _, ok := file.(*exactSizeObj); ok || args.Redirect {\n\t\treturn &model.Link{URL: file.GetPath()}, nil\n\t}\n\tres, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetDoNotParseResponse(true).\n\t\tHead(file.GetPath())\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to head [%s]\", file.GetPath())\n\t}\n\t_ = res.RawResponse.Body.Close()\n\treturn &model.Link{\n\t\tURL:           file.GetPath(),\n\t\tContentLength: res.RawResponse.ContentLength,\n\t}, nil\n}\n\nvar _ driver.Driver = (*AutoIndex)(nil)\n"
  },
  {
    "path": "drivers/autoindex/meta.go",
    "content": "package autoindex\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tURL                string `json:\"url\" required:\"true\"`\n\tItemXPath          string `json:\"item_xpath\" required:\"true\"`\n\tNameXPath          string `json:\"name_xpath\" required:\"true\"`\n\tModifiedXPath      string `json:\"modified_xpath\"`\n\tSizeXPath          string `json:\"size_xpath\"`\n\tIgnoreFileNames    string `json:\"ignore_file_names\" type:\"text\" default:\".\\n..\\nParent Directory\\nUp\"`\n\tModifiedTimeFormat string `json:\"modified_time_format\" default:\"02-Jan-2006 15:04\" help:\"Must be based on the time point Mon Jan 2 15:04:05 -0700 MST 2006\"`\n}\n\nvar config = driver.Config{\n\tName:        \"AutoIndex\",\n\tLocalSort:   true,\n\tCheckStatus: true,\n\tNoUpload:    true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AutoIndex{}\n\t})\n}\n"
  },
  {
    "path": "drivers/autoindex/types.go",
    "content": "package autoindex\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nvar (\n\terrEmptyEvaluateResult = fmt.Errorf(\"empty result\")\n)\n\ntype exactSizeObj struct{ model.Obj }\n"
  },
  {
    "path": "drivers/autoindex/util.go",
    "content": "package autoindex\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/antchfx/xpath\"\n\t\"github.com/pkg/errors\"\n)\n\nvar units = map[string]int64{\n\t\"\":      1,\n\t\"b\":     1,\n\t\"byte\":  1,\n\t\"bytes\": 1,\n\t\"k\":     1 << 10,\n\t\"kb\":    1 << 10,\n\t\"kib\":   1 << 10,\n\t\"m\":     1 << 20,\n\t\"mb\":    1 << 20,\n\t\"mib\":   1 << 20,\n\t\"g\":     1 << 30,\n\t\"gb\":    1 << 30,\n\t\"gib\":   1 << 30,\n\t\"t\":     1 << 40,\n\t\"tb\":    1 << 40,\n\t\"tib\":   1 << 40,\n\t\"p\":     1 << 50,\n\t\"pb\":    1 << 50,\n\t\"pib\":   1 << 50,\n}\n\nfunc splitUnit(s string) (string, string) {\n\tfor i := len(s) - 1; i >= 0; i-- {\n\t\tif s[i] >= '0' && s[i] <= '9' {\n\t\t\treturn strings.TrimSpace(s[:i+1]), strings.TrimSpace(s[i+1:])\n\t\t}\n\t}\n\treturn \"\", s\n}\n\nfunc parseSize(a any) (int64, bool, error) {\n\t// 第二个返回值exact表示大小是否精确\n\tif f, ok := a.(float64); ok {\n\t\treturn int64(f), false, nil\n\t}\n\ts, err := parseString(a)\n\tif errors.Is(err, errEmptyEvaluateResult) {\n\t\t// 可能是错误，也可能确实大小为0\n\t\t// 如果确实大小为0，大概率不会下载，exact返回false也不会有什么性能损失\n\t\t// 如果是错误，exact返回true会导致本地代理出错，综合来看返回false更好\n\t\treturn 0, false, nil\n\t}\n\tif err != nil {\n\t\treturn 0, false, err\n\t}\n\ts = strings.TrimSpace(s)\n\tif s == \"-\" {\n\t\treturn 0, false, nil\n\t}\n\tnbs, unit := splitUnit(s)\n\tmul, ok := units[strings.ToLower(unit)]\n\texact := mul == 1\n\tif !ok {\n\t\tmul = 1\n\t\t// 推测无单位，exact应为false\n\t}\n\tnb, err := strconv.ParseInt(nbs, 10, 64)\n\tif err != nil {\n\t\tfnb, err := strconv.ParseFloat(nbs, 64)\n\t\tif err != nil {\n\t\t\treturn 0, false, fmt.Errorf(\"failed to convert %s to number\", nbs)\n\t\t}\n\t\tnb = int64(fnb * float64(mul))\n\t\texact = false\n\t} else {\n\t\tnb = nb * mul\n\t}\n\treturn nb, exact, nil\n}\n\nfunc parseString(res any) (string, error) {\n\tif r, ok := res.(string); ok {\n\t\tif len(r) == 0 {\n\t\t\treturn \"\", errEmptyEvaluateResult\n\t\t}\n\t\treturn r, nil\n\t}\n\tn, ok := res.(*xpath.NodeIterator)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported evaluating result\")\n\t}\n\tif !n.MoveNext() {\n\t\treturn \"\", fmt.Errorf(\"no matched nodes\")\n\t}\n\tns := n.Current().Value()\n\tif len(ns) == 0 {\n\t\treturn \"\", errEmptyEvaluateResult\n\t}\n\treturn ns, nil\n}\n\nfunc parseTime(res any, format string) (time.Time, error) {\n\ts, err := parseString(res)\n\tif err != nil {\n\t\treturn time.Now(), err\n\t}\n\ts = strings.TrimSpace(s)\n\tt, err := time.Parse(format, s)\n\tif err != nil {\n\t\treturn time.Now(), errors.WithMessagef(err, \"failed to convert %s to time\", s)\n\t}\n\treturn t, nil\n}\n"
  },
  {
    "path": "drivers/autoindex/util_test.go",
    "content": "package autoindex\n\nimport (\n\t\"testing\"\n)\n\ntype wantType struct {\n\tv     int64\n\texact bool\n\terror bool\n}\n\nfunc TestParseSize(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\twant  wantType\n\t}{\n\t\t{\"100\", wantType{100, true, false}},\n\t\t{\"1k\", wantType{1024, false, false}},\n\t\t{\"1kb\", wantType{1024, false, false}},\n\t\t{\"1K\", wantType{1024, false, false}},      // case insensitive\n\t\t{\"1.5m\", wantType{1572864, false, false}}, // 1.5 * 1024^2\n\t\t{\"500 bytes\", wantType{500, true, false}},\n\t\t{\"-\", wantType{0, false, false}},\n\t\t{\"\", wantType{0, false, false}},\n\t\t{\"abc\", wantType{0, false, true}},\n\t\t{\"1.5GB\", wantType{1610612736, false, false}},    // 1.5 * 1024^3\n\t\t{\"2t\", wantType{2199023255552, false, false}},    // 2 * 1024^4\n\t\t{\"1p\", wantType{1125899906842624, false, false}}, // 1 * 1024^5\n\t\t{\"0\", wantType{0, true, false}},\n\t\t{\"  100  \", wantType{100, true, false}}, // trimmed\n\t\t{\"100b\", wantType{100, true, false}},\n\t\t{\"1gib\", wantType{1073741824, false, false}}, // 1024^3\n\t\t{\"1z\", wantType{1, false, false}},            // invalid unit, mul=1\n\t\t{\"1.5\", wantType{1, false, false}},           // float without unit, truncated\n\t\t{\"2.7k\", wantType{2764, false, false}},       // 2.7 * 1024 truncated\n\t\t{\"1.0g\", wantType{1073741824, false, false}}, // 1.0 * 1024^3\n\t\t{\"invalid\", wantType{0, false, true}},\n\t\t{\"123xyz\", wantType{123, false, false}}, // unit not found, mul=1\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tgot, exact, err := parseSize(tt.input)\n\t\t\tif got != tt.want.v || exact != tt.want.exact || (err != nil) != tt.want.error {\n\t\t\t\tt.Errorf(\"ParseSize(%q) = (%d, %t, %t), want (%d, %t, %t)\", tt.input, got, exact, err != nil, tt.want.v, tt.want.exact, tt.want.error)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "drivers/azure_blob/driver.go",
    "content": "package azure_blob\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\n// Azure Blob Storage based on the blob APIs\n// Link: https://learn.microsoft.com/rest/api/storageservices/blob-service-rest-api\ntype AzureBlob struct {\n\tmodel.Storage\n\tAddition\n\tclient          *azblob.Client\n\tcontainerClient *container.Client\n}\n\n// Config returns the driver configuration.\nfunc (d *AzureBlob) Config() driver.Config {\n\treturn config\n}\n\n// GetAddition returns additional settings specific to Azure Blob Storage.\nfunc (d *AzureBlob) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\n// Init initializes the Azure Blob Storage client using shared key authentication.\nfunc (d *AzureBlob) Init(ctx context.Context) error {\n\t// Validate the endpoint URL\n\taccountName := extractAccountName(d.Addition.Endpoint)\n\tif !regexp.MustCompile(`^[a-z0-9]+$`).MatchString(accountName) {\n\t\treturn fmt.Errorf(\"invalid storage account name: must be chars of lowercase letters or numbers only\")\n\t}\n\n\tcredential, err := azblob.NewSharedKeyCredential(accountName, d.Addition.AccessKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create credential: %w\", err)\n\t}\n\n\t// Check if Endpoint is just account name\n\tendpoint := d.Addition.Endpoint\n\tif accountName == endpoint {\n\t\tendpoint = fmt.Sprintf(\"https://%s.blob.core.windows.net/\", accountName)\n\t}\n\t// Initialize Azure Blob client with retry policy\n\tclient, err := azblob.NewClientWithSharedKeyCredential(endpoint, credential,\n\t\t&azblob.ClientOptions{ClientOptions: azcore.ClientOptions{\n\t\t\tRetry: policy.RetryOptions{\n\t\t\t\tMaxRetries: MaxRetries,\n\t\t\t\tRetryDelay: RetryDelay,\n\t\t\t},\n\t\t}})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create client: %w\", err)\n\t}\n\td.client = client\n\n\t// Ensure container exists or create it\n\tcontainerName := strings.Trim(d.Addition.ContainerName, \"/ \\\\\")\n\tif containerName == \"\" {\n\t\treturn fmt.Errorf(\"container name cannot be empty\")\n\t}\n\treturn d.createContainerIfNotExists(ctx, containerName)\n}\n\n// Drop releases resources associated with the Azure Blob client.\nfunc (d *AzureBlob) Drop(ctx context.Context) error {\n\td.client = nil\n\treturn nil\n}\n\n// List retrieves blobs and directories under the specified path.\nfunc (d *AzureBlob) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tprefix := ensureTrailingSlash(dir.GetPath())\n\tif prefix == \"/\" {\n\t\tprefix = \"\"\n\t}\n\n\tpager := d.containerClient.NewListBlobsHierarchyPager(\"/\", &container.ListBlobsHierarchyOptions{\n\t\tPrefix: &prefix,\n\t})\n\n\tvar objs []model.Obj\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list blobs: %w\", err)\n\t\t}\n\n\t\t// Process directories\n\t\tfor _, blobPrefix := range page.Segment.BlobPrefixes {\n\t\t\tobjs = append(objs, &model.Object{\n\t\t\t\tName: path.Base(strings.TrimSuffix(*blobPrefix.Name, \"/\")),\n\t\t\t\tPath: *blobPrefix.Name,\n\t\t\t\t// Azure does not support properties now.\n\t\t\t\t//Modified: *blobPrefix.Properties.LastModified,\n\t\t\t\t//Ctime:    *blobPrefix.Properties.CreationTime,\n\t\t\t\tIsFolder: true,\n\t\t\t})\n\t\t}\n\n\t\t// Process files\n\t\tfor _, blob := range page.Segment.BlobItems {\n\t\t\tif strings.HasSuffix(*blob.Name, \"/\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tobjs = append(objs, &model.Object{\n\t\t\t\tName:     path.Base(*blob.Name),\n\t\t\t\tPath:     *blob.Name,\n\t\t\t\tSize:     *blob.Properties.ContentLength,\n\t\t\t\tModified: *blob.Properties.LastModified,\n\t\t\t\tCtime:    *blob.Properties.CreationTime,\n\t\t\t\tIsFolder: false,\n\t\t\t})\n\t\t}\n\t}\n\treturn objs, nil\n}\n\n// Link generates a temporary SAS URL for accessing a blob.\nfunc (d *AzureBlob) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tblobClient := d.containerClient.NewBlobClient(file.GetPath())\n\texpireDuration := time.Hour * time.Duration(d.SignURLExpire)\n\n\tsasURL, err := blobClient.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(expireDuration), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate SAS URL: %w\", err)\n\t}\n\treturn &model.Link{URL: sasURL}, nil\n}\n\n// MakeDir creates a virtual directory by uploading an empty blob as a marker.\nfunc (d *AzureBlob) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tdirPath := path.Join(parentDir.GetPath(), dirName)\n\tif err := d.mkDir(ctx, dirPath); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create directory marker: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tPath:     dirPath,\n\t\tName:     dirName,\n\t\tIsFolder: true,\n\t}, nil\n}\n\n// Move relocates an object (file or directory) to a new directory.\nfunc (d *AzureBlob) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tsrcPath := srcObj.GetPath()\n\tdstPath := path.Join(dstDir.GetPath(), srcObj.GetName())\n\n\tif err := d.moveOrRename(ctx, srcPath, dstPath, srcObj.IsDir(), srcObj.GetSize()); err != nil {\n\t\treturn nil, fmt.Errorf(\"move operation failed: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tPath:     dstPath,\n\t\tName:     srcObj.GetName(),\n\t\tModified: time.Now(),\n\t\tIsFolder: srcObj.IsDir(),\n\t\tSize:     srcObj.GetSize(),\n\t}, nil\n}\n\n// Rename changes the name of an existing object.\nfunc (d *AzureBlob) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tsrcPath := srcObj.GetPath()\n\tdstPath := path.Join(path.Dir(srcPath), newName)\n\n\tif err := d.moveOrRename(ctx, srcPath, dstPath, srcObj.IsDir(), srcObj.GetSize()); err != nil {\n\t\treturn nil, fmt.Errorf(\"rename operation failed: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tPath:     dstPath,\n\t\tName:     newName,\n\t\tModified: time.Now(),\n\t\tIsFolder: srcObj.IsDir(),\n\t\tSize:     srcObj.GetSize(),\n\t}, nil\n}\n\n// Copy duplicates an object (file or directory) to a specified destination directory.\nfunc (d *AzureBlob) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tdstPath := path.Join(dstDir.GetPath(), srcObj.GetName())\n\n\t// Handle directory copying using flat listing\n\tif srcObj.IsDir() {\n\t\tsrcPrefix := srcObj.GetPath()\n\t\tsrcPrefix = ensureTrailingSlash(srcPrefix)\n\n\t\t// Get all blobs under the source directory\n\t\tblobs, err := d.flattenListBlobs(ctx, srcPrefix)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list source directory contents: %w\", err)\n\t\t}\n\n\t\t// Process each blob - copy to destination\n\t\tfor _, blob := range blobs {\n\t\t\t// Skip the directory marker itself\n\t\t\tif *blob.Name == srcPrefix {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Calculate relative path from source\n\t\t\trelPath := strings.TrimPrefix(*blob.Name, srcPrefix)\n\t\t\titemDstPath := path.Join(dstPath, relPath)\n\n\t\t\tif strings.HasSuffix(itemDstPath, \"/\") || (blob.Metadata[\"hdi_isfolder\"] != nil && *blob.Metadata[\"hdi_isfolder\"] == \"true\") {\n\t\t\t\t// Create directory marker at destination\n\t\t\t\terr := d.mkDir(ctx, itemDstPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to create directory marker [%s]: %w\", itemDstPath, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Copy the blob\n\t\t\t\tif err := d.copyFile(ctx, *blob.Name, itemDstPath); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to copy %s: %w\", *blob.Name, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t// Create directory marker at destination if needed\n\t\tif len(blobs) == 0 {\n\t\t\terr := d.mkDir(ctx, dstPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create directory [%s]: %w\", dstPath, err)\n\t\t\t}\n\t\t}\n\n\t\treturn &model.Object{\n\t\t\tPath:     dstPath,\n\t\t\tName:     srcObj.GetName(),\n\t\t\tModified: time.Now(),\n\t\t\tIsFolder: true,\n\t\t}, nil\n\t}\n\n\t// Copy a single file\n\tif err := d.copyFile(ctx, srcObj.GetPath(), dstPath); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to copy blob: %w\", err)\n\t}\n\treturn &model.Object{\n\t\tPath:     dstPath,\n\t\tName:     srcObj.GetName(),\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: time.Now(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\n// Remove deletes a specified blob or recursively deletes a directory and its contents.\nfunc (d *AzureBlob) Remove(ctx context.Context, obj model.Obj) error {\n\tpath := obj.GetPath()\n\n\t// Handle recursive directory deletion\n\tif obj.IsDir() {\n\t\treturn d.deleteFolder(ctx, path)\n\t}\n\n\t// Delete single file\n\treturn d.deleteFile(ctx, path, false)\n}\n\n// Put uploads a file stream to Azure Blob Storage with progress tracking.\nfunc (d *AzureBlob) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tblobPath := path.Join(dstDir.GetPath(), stream.GetName())\n\tblobClient := d.containerClient.NewBlockBlobClient(blobPath)\n\n\t// Determine optimal upload options based on file size\n\toptions := optimizedUploadOptions(stream.GetSize())\n\n\t// Track upload progress\n\tprogressTracker := &progressTracker{\n\t\ttotal:          stream.GetSize(),\n\t\tupdateProgress: up,\n\t}\n\n\t// Wrap stream to handle context cancellation and progress tracking\n\tlimitedStream := driver.NewLimitedUploadStream(ctx, io.TeeReader(stream, progressTracker))\n\n\t// Upload the stream to Azure Blob Storage\n\t_, err := blobClient.UploadStream(ctx, limitedStream, options)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload file: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tPath:     blobPath,\n\t\tName:     stream.GetName(),\n\t\tSize:     stream.GetSize(),\n\t\tModified: time.Now(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\n// The following methods related to archive handling are not implemented yet.\n// func (d *AzureBlob) GetArchiveMeta(...) {...}\n// func (d *AzureBlob) ListArchive(...) {...}\n// func (d *AzureBlob) Extract(...) {...}\n// func (d *AzureBlob) ArchiveDecompress(...) {...}\n\n// Ensure AzureBlob implements the driver.Driver interface.\nvar _ driver.Driver = (*AzureBlob)(nil)\n"
  },
  {
    "path": "drivers/azure_blob/meta.go",
    "content": "package azure_blob\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tEndpoint      string `json:\"endpoint\" required:\"true\" default:\"https://<accountname>.blob.core.windows.net/\" help:\"e.g. https://accountname.blob.core.windows.net/. The full endpoint URL for Azure Storage, including the unique storage account name (3 ~ 24 numbers and lowercase letters only).\"`\n\tAccessKey     string `json:\"access_key\" required:\"true\" help:\"The access key for Azure Storage, used for authentication. https://learn.microsoft.com/azure/storage/common/storage-account-keys-manage\"`\n\tContainerName string `json:\"container_name\" required:\"true\" help:\"The name of the container in Azure Storage (created in the Azure portal). https://learn.microsoft.com/azure/storage/blobs/blob-containers-portal\"`\n\tSignURLExpire int    `json:\"sign_url_expire\" type:\"number\" default:\"4\" help:\"The expiration time for SAS URLs, in hours.\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Azure Blob Storage\",\n\tLocalSort:   true,\n\tCheckStatus: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AzureBlob{}\n\t})\n}\n"
  },
  {
    "path": "drivers/azure_blob/types.go",
    "content": "package azure_blob\n\nimport \"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\n// progressTracker is used to track upload progress\ntype progressTracker struct {\n\ttotal          int64\n\tcurrent        int64\n\tupdateProgress driver.UpdateProgress\n}\n\n// Write implements io.Writer to track progress\nfunc (pt *progressTracker) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\tpt.current += int64(n)\n\tif pt.updateProgress != nil && pt.total > 0 {\n\t\tpt.updateProgress(float64(pt.current) * 100 / float64(pt.total))\n\t}\n\treturn n, nil\n}\n"
  },
  {
    "path": "drivers/azure_blob/util.go",
    "content": "package azure_blob\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\t// MaxRetries defines the maximum number of retry attempts for Azure operations\n\tMaxRetries = 3\n\t// RetryDelay defines the base delay between retries\n\tRetryDelay = 3 * time.Second\n\t// MaxBatchSize defines the maximum number of operations in a single batch request\n\tMaxBatchSize = 128\n)\n\n// extractAccountName 从 Azure 存储 Endpoint 中提取账户名\nfunc extractAccountName(endpoint string) string {\n\t// 移除协议前缀\n\tendpoint = strings.TrimPrefix(endpoint, \"https://\")\n\tendpoint = strings.TrimPrefix(endpoint, \"http://\")\n\n\t// 获取第一个点之前的部分（即账户名）\n\tparts := strings.Split(endpoint, \".\")\n\tif len(parts) > 0 {\n\t\t// to lower case\n\t\treturn strings.ToLower(parts[0])\n\t}\n\treturn \"\"\n}\n\n// isNotFoundError checks if the error is a \"not found\" type error\nfunc isNotFoundError(err error) bool {\n\tvar storageErr *azcore.ResponseError\n\tif errors.As(err, &storageErr) {\n\t\treturn storageErr.StatusCode == 404\n\t}\n\t// Fallback to string matching for backwards compatibility\n\treturn err != nil && strings.Contains(err.Error(), \"BlobNotFound\")\n}\n\n// flattenListBlobs - Optimize blob listing to handle pagination better\nfunc (d *AzureBlob) flattenListBlobs(ctx context.Context, prefix string) ([]container.BlobItem, error) {\n\t// Standardize prefix format\n\tprefix = ensureTrailingSlash(prefix)\n\n\tvar blobItems []container.BlobItem\n\tpager := d.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{\n\t\tPrefix: &prefix,\n\t\tInclude: container.ListBlobsInclude{\n\t\t\tMetadata: true,\n\t\t},\n\t})\n\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list blobs: %w\", err)\n\t\t}\n\n\t\tfor _, blob := range page.Segment.BlobItems {\n\t\t\tblobItems = append(blobItems, *blob)\n\t\t}\n\t}\n\n\treturn blobItems, nil\n}\n\n// batchDeleteBlobs - Simplify batch deletion logic\nfunc (d *AzureBlob) batchDeleteBlobs(ctx context.Context, blobPaths []string) error {\n\tif len(blobPaths) == 0 {\n\t\treturn nil\n\t}\n\n\t// Process in batches of MaxBatchSize\n\tfor i := 0; i < len(blobPaths); i += MaxBatchSize {\n\t\tend := min(i+MaxBatchSize, len(blobPaths))\n\t\tcurrentBatch := blobPaths[i:end]\n\n\t\t// Create batch builder\n\t\tbatchBuilder, err := d.containerClient.NewBatchBuilder()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create batch builder: %w\", err)\n\t\t}\n\n\t\t// Add delete operations\n\t\tfor _, blobPath := range currentBatch {\n\t\t\tif err := batchBuilder.Delete(blobPath, nil); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to add delete operation for %s: %w\", blobPath, err)\n\t\t\t}\n\t\t}\n\n\t\t// Submit batch\n\t\tresponses, err := d.containerClient.SubmitBatch(ctx, batchBuilder, nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"batch delete request failed: %w\", err)\n\t\t}\n\n\t\t// Check responses\n\t\tfor _, resp := range responses.Responses {\n\t\t\tif resp.Error != nil && !isNotFoundError(resp.Error) {\n\t\t\t\t// 获取 blob 名称以提供更好的错误信息\n\t\t\t\tblobName := \"unknown\"\n\t\t\t\tif resp.BlobName != nil {\n\t\t\t\t\tblobName = *resp.BlobName\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"failed to delete blob %s: %v\", blobName, resp.Error)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// deleteFolder recursively deletes a directory and all its contents\nfunc (d *AzureBlob) deleteFolder(ctx context.Context, prefix string) error {\n\t// Ensure directory path ends with slash\n\tprefix = ensureTrailingSlash(prefix)\n\n\t// Get all blobs under the directory using flattenListBlobs\n\tglobs, err := d.flattenListBlobs(ctx, prefix)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list blobs for deletion: %w\", err)\n\t}\n\n\t// If there are blobs in the directory, delete them\n\tif len(globs) > 0 {\n\t\t// 分离文件和目录标记\n\t\tvar filePaths []string\n\t\tvar dirPaths []string\n\n\t\tfor _, blob := range globs {\n\t\t\tblobName := *blob.Name\n\t\t\tif isDirectory(blob) {\n\t\t\t\t// remove trailing slash for directory names\n\t\t\t\tdirPaths = append(dirPaths, strings.TrimSuffix(blobName, \"/\"))\n\t\t\t} else {\n\t\t\t\tfilePaths = append(filePaths, blobName)\n\t\t\t}\n\t\t}\n\n\t\t// 先删除文件，再删除目录\n\t\tif len(filePaths) > 0 {\n\t\t\tif err := d.batchDeleteBlobs(ctx, filePaths); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(dirPaths) > 0 {\n\t\t\t// 按路径深度分组\n\t\t\tdepthMap := make(map[int][]string)\n\t\t\tfor _, dir := range dirPaths {\n\t\t\t\tdepth := strings.Count(dir, \"/\") // 计算目录深度\n\t\t\t\tdepthMap[depth] = append(depthMap[depth], dir)\n\t\t\t}\n\n\t\t\t// 按深度从大到小排序\n\t\t\tvar depths []int\n\t\t\tfor depth := range depthMap {\n\t\t\t\tdepths = append(depths, depth)\n\t\t\t}\n\t\t\tsort.Sort(sort.Reverse(sort.IntSlice(depths)))\n\n\t\t\t// 按深度逐层批量删除\n\t\t\tfor _, depth := range depths {\n\t\t\t\tbatch := depthMap[depth]\n\t\t\t\tif err := d.batchDeleteBlobs(ctx, batch); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 最后删除目录标记本身\n\treturn d.deleteEmptyDirectory(ctx, prefix)\n}\n\n// deleteFile deletes a single file or blob with better error handling\nfunc (d *AzureBlob) deleteFile(ctx context.Context, path string, isDir bool) error {\n\tblobClient := d.containerClient.NewBlobClient(path)\n\t_, err := blobClient.Delete(ctx, nil)\n\tif err != nil && !(isDir && isNotFoundError(err)) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// copyFile copies a single blob from source path to destination path\nfunc (d *AzureBlob) copyFile(ctx context.Context, srcPath, dstPath string) error {\n\tsrcBlob := d.containerClient.NewBlobClient(srcPath)\n\tdstBlob := d.containerClient.NewBlobClient(dstPath)\n\n\t// Use configured expiration time for SAS URL\n\texpireDuration := time.Hour * time.Duration(d.SignURLExpire)\n\tsrcURL, err := srcBlob.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(expireDuration), nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate source SAS URL: %w\", err)\n\t}\n\n\t_, err = dstBlob.StartCopyFromURL(ctx, srcURL, nil)\n\treturn err\n\n}\n\n// createContainerIfNotExists - Create container if not exists\n// Clean up commented code\nfunc (d *AzureBlob) createContainerIfNotExists(ctx context.Context, containerName string) error {\n\tserviceClient := d.client.ServiceClient()\n\tcontainerClient := serviceClient.NewContainerClient(containerName)\n\n\tvar options = service.CreateContainerOptions{}\n\t_, err := containerClient.Create(ctx, &options)\n\tif err != nil {\n\t\tvar responseErr *azcore.ResponseError\n\t\tif errors.As(err, &responseErr) && responseErr.ErrorCode != \"ContainerAlreadyExists\" {\n\t\t\treturn fmt.Errorf(\"failed to create or access container [%s]: %w\", containerName, err)\n\t\t}\n\t}\n\n\td.containerClient = containerClient\n\treturn nil\n}\n\n// mkDir creates a virtual directory marker by uploading an empty blob with metadata.\nfunc (d *AzureBlob) mkDir(ctx context.Context, fullDirName string) error {\n\tdirPath := ensureTrailingSlash(fullDirName)\n\tblobClient := d.containerClient.NewBlockBlobClient(dirPath)\n\n\t// Upload an empty blob with metadata indicating it's a directory\n\t_, err := blobClient.Upload(ctx, struct {\n\t\t*bytes.Reader\n\t\tio.Closer\n\t}{\n\t\tReader: bytes.NewReader([]byte{}),\n\t\tCloser: io.NopCloser(nil),\n\t}, &blockblob.UploadOptions{\n\t\tMetadata: map[string]*string{\n\t\t\t\"hdi_isfolder\": to.Ptr(\"true\"),\n\t\t},\n\t})\n\treturn err\n}\n\n// ensureTrailingSlash ensures the provided path ends with a trailing slash.\nfunc ensureTrailingSlash(path string) string {\n\tif !strings.HasSuffix(path, \"/\") {\n\t\treturn path + \"/\"\n\t}\n\treturn path\n}\n\n// moveOrRename moves or renames blobs or directories from source to destination.\nfunc (d *AzureBlob) moveOrRename(ctx context.Context, srcPath, dstPath string, isDir bool, srcSize int64) error {\n\tif isDir {\n\t\t// Normalize paths for directory operations\n\t\tsrcPath = ensureTrailingSlash(srcPath)\n\t\tdstPath = ensureTrailingSlash(dstPath)\n\n\t\t// List all blobs under the source directory\n\t\tblobs, err := d.flattenListBlobs(ctx, srcPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to list blobs: %w\", err)\n\t\t}\n\n\t\t// Iterate and copy each blob to the destination\n\t\tfor _, item := range blobs {\n\t\t\tsrcBlobName := *item.Name\n\t\t\trelPath := strings.TrimPrefix(srcBlobName, srcPath)\n\t\t\titemDstPath := path.Join(dstPath, relPath)\n\n\t\t\tif isDirectory(item) {\n\t\t\t\t// Create directory marker at destination\n\t\t\t\tif err := d.mkDir(ctx, itemDstPath); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create directory marker [%s]: %w\", itemDstPath, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Copy file blob to destination\n\t\t\t\tif err := d.copyFile(ctx, srcBlobName, itemDstPath); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to copy blob [%s]: %w\", srcBlobName, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle empty directories by creating a marker at destination\n\t\tif len(blobs) == 0 {\n\t\t\tif err := d.mkDir(ctx, dstPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create directory [%s]: %w\", dstPath, err)\n\t\t\t}\n\t\t}\n\n\t\t// Delete source directory and its contents\n\t\tif err := d.deleteFolder(ctx, srcPath); err != nil {\n\t\t\tlog.Warnf(\"failed to delete source directory [%s]: %v\\n, and try again\", srcPath, err)\n\t\t\t// Retry deletion once more and ignore the result\n\t\t\tif err := d.deleteFolder(ctx, srcPath); err != nil {\n\t\t\t\tlog.Errorf(\"Retry deletion of source directory [%s] failed: %v\", srcPath, err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Single file move or rename operation\n\tif err := d.copyFile(ctx, srcPath, dstPath); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy file: %w\", err)\n\t}\n\n\t// Delete source file after successful copy\n\tif err := d.deleteFile(ctx, srcPath, false); err != nil {\n\t\tlog.Errorf(\"Error deleting source file [%s]: %v\", srcPath, err)\n\t}\n\treturn nil\n}\n\n// optimizedUploadOptions returns the optimal upload options based on file size\nfunc optimizedUploadOptions(fileSize int64) *azblob.UploadStreamOptions {\n\toptions := &azblob.UploadStreamOptions{\n\t\tBlockSize:   4 * 1024 * 1024, // 4MB block size\n\t\tConcurrency: 4,               // Default concurrency\n\t}\n\n\t// For large files, increase block size and concurrency\n\tif fileSize > 256*1024*1024 { // For files larger than 256MB\n\t\toptions.BlockSize = 8 * 1024 * 1024 // 8MB blocks\n\t\toptions.Concurrency = 8             // More concurrent uploads\n\t}\n\n\t// For very large files (>1GB)\n\tif fileSize > 1024*1024*1024 {\n\t\toptions.BlockSize = 16 * 1024 * 1024 // 16MB blocks\n\t\toptions.Concurrency = 16             // Higher concurrency\n\t}\n\n\treturn options\n}\n\n// isDirectory determines if a blob represents a directory\n// Checks multiple indicators: path suffix, metadata, and content type\nfunc isDirectory(blob container.BlobItem) bool {\n\t// Check path suffix\n\tif strings.HasSuffix(*blob.Name, \"/\") {\n\t\treturn true\n\t}\n\n\t// Check metadata for directory marker\n\tif blob.Metadata != nil {\n\t\tif val, ok := blob.Metadata[\"hdi_isfolder\"]; ok && val != nil && *val == \"true\" {\n\t\t\treturn true\n\t\t}\n\t\t// Azure Storage Explorer and other tools may use different metadata keys\n\t\tif val, ok := blob.Metadata[\"is_directory\"]; ok && val != nil && strings.ToLower(*val) == \"true\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Check content type (some tools mark directories with specific content types)\n\tif blob.Properties != nil && blob.Properties.ContentType != nil {\n\t\tcontentType := strings.ToLower(*blob.Properties.ContentType)\n\t\tif blob.Properties.ContentLength != nil && *blob.Properties.ContentLength == 0 && (contentType == \"application/directory\" || contentType == \"directory\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// deleteEmptyDirectory deletes a directory only if it's empty\nfunc (d *AzureBlob) deleteEmptyDirectory(ctx context.Context, dirPath string) error {\n\t// Directory is empty, delete the directory marker\n\tblobClient := d.containerClient.NewBlobClient(strings.TrimSuffix(dirPath, \"/\"))\n\t_, err := blobClient.Delete(ctx, nil)\n\n\t// Also try deleting with trailing slash (for different directory marker formats)\n\tif err != nil && isNotFoundError(err) {\n\t\tblobClient = d.containerClient.NewBlobClient(dirPath)\n\t\t_, err = blobClient.Delete(ctx, nil)\n\t}\n\n\t// Ignore not found errors\n\tif err != nil && isNotFoundError(err) {\n\t\tlog.Infof(\"Directory [%s] not found during deletion: %v\", dirPath, err)\n\t\treturn nil\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "drivers/baidu_netdisk/driver.go",
    "content": "package baidu_netdisk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype BaiduNetdisk struct {\n\tmodel.Storage\n\tAddition\n\n\tuploadThread int\n\tvipType      int // 会员类型，0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)\n}\n\nvar ErrUploadIDExpired = errors.New(\"uploadid expired\")\n\nfunc (d *BaiduNetdisk) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *BaiduNetdisk) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *BaiduNetdisk) Init(ctx context.Context) error {\n\td.uploadThread, _ = strconv.Atoi(d.UploadThread)\n\tif d.uploadThread < 1 {\n\t\td.uploadThread, d.UploadThread = 1, \"1\"\n\t} else if d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 32, \"32\"\n\t}\n\n\tif _, err := url.Parse(d.UploadAPI); d.UploadAPI == \"\" || err != nil {\n\t\td.UploadAPI = UPLOAD_FALLBACK_API\n\t}\n\n\tres, err := d.get(\"/xpan/nas\", map[string]string{\n\t\t\"method\": \"uinfo\",\n\t}, nil)\n\tlog.Debugf(\"[baidu_netdisk] get uinfo: %s\", string(res))\n\tif err != nil {\n\t\treturn err\n\t}\n\td.vipType = utils.Json.Get(res, \"vip_type\").ToInt()\n\treturn nil\n}\n\nfunc (d *BaiduNetdisk) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *BaiduNetdisk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tswitch d.DownloadAPI {\n\tcase \"crack\":\n\t\treturn d.linkCrack(file, args)\n\tcase \"crack_video\":\n\t\treturn d.linkCrackVideo(file, args)\n\t}\n\treturn d.linkOfficial(file, args)\n}\n\nfunc (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar newDir File\n\t_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, \"\", \"\", &newDir, 0, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fileToObj(newDir), nil\n}\n\nfunc (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"dest\":    dstDir.GetPath(),\n\t\t\t\"newname\": srcObj.GetName(),\n\t\t},\n\t}\n\t_, err := d.manage(\"move\", data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif srcObj, ok := srcObj.(*model.ObjThumb); ok {\n\t\tsrcObj.SetPath(stdpath.Join(dstDir.GetPath(), srcObj.GetName()))\n\t\tsrcObj.Modified = time.Now()\n\t\treturn srcObj, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"newname\": newName,\n\t\t},\n\t}\n\t_, err := d.manage(\"rename\", data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif srcObj, ok := srcObj.(*model.ObjThumb); ok {\n\t\tsrcObj.SetPath(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))\n\t\tsrcObj.Name = newName\n\t\tsrcObj.Modified = time.Now()\n\t\treturn srcObj, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"dest\":    dstDir.GetPath(),\n\t\t\t\"newname\": srcObj.GetName(),\n\t\t},\n\t}\n\t_, err := d.manage(\"copy\", data)\n\treturn err\n}\n\nfunc (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error {\n\tdata := []string{obj.GetPath()}\n\t_, err := d.manage(\"delete\", data)\n\treturn err\n}\n\nfunc (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {\n\tcontentMd5 := stream.GetHash().GetHash(utils.MD5)\n\tif len(contentMd5) < utils.MD5.Width {\n\t\treturn nil, errors.New(\"invalid hash\")\n\t}\n\n\tstreamSize := stream.GetSize()\n\tpath := stdpath.Join(dstDir.GetPath(), stream.GetName())\n\tmtime := stream.ModTime().Unix()\n\tctime := stream.CreateTime().Unix()\n\tblockList, _ := utils.Json.MarshalToString([]string{contentMd5})\n\n\tvar newFile File\n\t_, err := d.create(path, streamSize, 0, \"\", blockList, &newFile, mtime, ctime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 修复时间，具体原因见 Put 方法注释的 **注意**\n\tnewFile.Ctime = stream.CreateTime().Unix()\n\tnewFile.Mtime = stream.ModTime().Unix()\n\treturn fileToObj(newFile), nil\n}\n\n// Put\n//\n// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间，而不是文件时间。\n// 而实际上云盘存储的时间是文件时间，所以此处需要覆盖时间，保证缓存与云盘的数据一致\nfunc (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// 百度网盘不允许上传空文件\n\tif stream.GetSize() < 1 {\n\t\treturn nil, ErrBaiduEmptyFilesNotAllowed\n\t}\n\n\t// rapid upload\n\tif newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {\n\t\treturn newObj, nil\n\t}\n\n\tvar (\n\t\tcache = stream.GetFile()\n\t\ttmpF  *os.File\n\t\terr   error\n\t)\n\tif cache == nil {\n\t\ttmpF, err = os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = tmpF.Close()\n\t\t\t_ = os.Remove(tmpF.Name())\n\t\t}()\n\t\tcache = tmpF\n\t}\n\n\tstreamSize := stream.GetSize()\n\tsliceSize := d.getSliceSize(streamSize)\n\tcount := 1\n\tif streamSize > sliceSize {\n\t\tcount = int((streamSize + sliceSize - 1) / sliceSize)\n\t}\n\tlastBlockSize := streamSize % sliceSize\n\tif lastBlockSize == 0 {\n\t\tlastBlockSize = sliceSize\n\t}\n\n\t// cal md5 for first 256k data\n\tconst SliceSize int64 = 256 * utils.KB\n\tblockList := make([]string, 0, count)\n\tbyteSize := sliceSize\n\tfileMd5H := md5.New()\n\tsliceMd5H := md5.New()\n\tsliceMd5H2 := md5.New()\n\tslicemd5H2Write := utils.LimitWriter(sliceMd5H2, SliceSize)\n\twriters := []io.Writer{fileMd5H, sliceMd5H, slicemd5H2Write}\n\tif tmpF != nil {\n\t\twriters = append(writers, tmpF)\n\t}\n\twritten := int64(0)\n\n\tfor i := 1; i <= count; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t\tif i == count {\n\t\t\tbyteSize = lastBlockSize\n\t\t}\n\t\tn, err := utils.CopyWithBufferN(io.MultiWriter(writers...), stream, byteSize)\n\t\twritten += n\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn nil, err\n\t\t}\n\t\tblockList = append(blockList, hex.EncodeToString(sliceMd5H.Sum(nil)))\n\t\tsliceMd5H.Reset()\n\t}\n\tif tmpF != nil {\n\t\tif written != streamSize {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, size mismatch: %d != %d \", written, streamSize)\n\t\t}\n\t\t_, err = tmpF.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, can't seek to 0 \")\n\t\t}\n\t}\n\tcontentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))\n\tsliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))\n\tblockListStr, _ := utils.Json.MarshalToString(blockList)\n\tpath := stdpath.Join(dstDir.GetPath(), stream.GetName())\n\tmtime := stream.ModTime().Unix()\n\tctime := stream.CreateTime().Unix()\n\n\t// step.1 尝试读取已保存进度\n\tprecreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)\n\tif !ok {\n\t\t// 没有进度，走预上传\n\t\tprecreateResp, err = d.precreate(ctx, path, streamSize, blockListStr, contentMd5, sliceMd5, ctime, mtime)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif precreateResp.ReturnType == 2 {\n\t\t\t// rapid upload, since got md5 match from baidu server\n\t\t\t// 修复时间，具体原因见 Put 方法注释的 **注意**\n\t\t\tprecreateResp.File.Ctime = ctime\n\t\t\tprecreateResp.File.Mtime = mtime\n\t\t\treturn fileToObj(precreateResp.File), nil\n\t\t}\n\t}\n\tensureUploadURL := func() {\n\t\tif precreateResp.UploadURL != \"\" {\n\t\t\treturn\n\t\t}\n\t\tprecreateResp.UploadURL = d.getUploadUrl(path, precreateResp.Uploadid)\n\t}\n\n\t// step.2 上传分片\nuploadLoop:\n\tfor range 2 {\n\t\t// 获取上传域名\n\t\tensureUploadURL()\n\t\t// 并发上传\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,\n\t\t\tretry.Attempts(UPLOAD_RETRY_COUNT),\n\t\t\tretry.Delay(UPLOAD_RETRY_WAIT_TIME),\n\t\t\tretry.MaxDelay(UPLOAD_RETRY_MAX_WAIT_TIME),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.RetryIf(func(err error) bool {\n\t\t\t\treturn !errors.Is(err, ErrUploadIDExpired)\n\t\t\t}),\n\t\t\tretry.LastErrorOnly(true))\n\n\t\ttotalParts := len(precreateResp.BlockList)\n\n\t\tfor i, partseq := range precreateResp.BlockList {\n\t\t\tif utils.IsCanceled(upCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif partseq < 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ti, partseq := i, partseq\n\t\t\toffset, size := int64(partseq)*sliceSize, sliceSize\n\t\t\tif partseq+1 == count {\n\t\t\t\tsize = lastBlockSize\n\t\t\t}\n\t\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t\tparams := map[string]string{\n\t\t\t\t\t\"method\":       \"upload\",\n\t\t\t\t\t\"access_token\": d.AccessToken,\n\t\t\t\t\t\"type\":         \"tmpfile\",\n\t\t\t\t\t\"path\":         path,\n\t\t\t\t\t\"uploadid\":     precreateResp.Uploadid,\n\t\t\t\t\t\"partseq\":      strconv.Itoa(partseq),\n\t\t\t\t}\n\t\t\t\tsection := io.NewSectionReader(cache, offset, size)\n\t\t\t\terr := d.uploadSlice(ctx, precreateResp.UploadURL, params, stream.GetName(), section)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tprecreateResp.BlockList[i] = -1\n\t\t\t\tprogress := float64(threadG.Success()+1) * 100 / float64(totalParts+1)\n\t\t\t\tup(progress)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\terr = threadG.Wait()\n\t\tif err == nil {\n\t\t\tbreak uploadLoop\n\t\t}\n\n\t\t// 保存进度（所有错误都会保存）\n\t\tprecreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })\n\t\tbase.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)\n\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\treturn nil, err\n\t\t}\n\t\tif errors.Is(err, ErrUploadIDExpired) {\n\t\t\tlog.Warn(\"[baidu_netdisk] uploadid expired, will restart from scratch\")\n\t\t\t// 重新 precreate（所有分片都要重传）\n\t\t\tnewPre, err2 := d.precreate(ctx, path, streamSize, blockListStr, \"\", \"\", ctime, mtime)\n\t\t\tif err2 != nil {\n\t\t\t\treturn nil, err2\n\t\t\t}\n\t\t\tif newPre.ReturnType == 2 {\n\t\t\t\treturn fileToObj(newPre.File), nil\n\t\t\t}\n\t\t\tprecreateResp = newPre\n\t\t\tprecreateResp.UploadURL = \"\"\n\t\t\t// 覆盖掉旧的进度\n\t\t\tbase.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)\n\t\t\tcontinue uploadLoop\n\t\t}\n\t\treturn nil, err\n\t}\n\tdefer up(100)\n\n\t// step.3 创建文件\n\tvar newFile File\n\t_, err = d.create(path, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 修复时间，具体原因见 Put 方法注释的 **注意**\n\tnewFile.Ctime = ctime\n\tnewFile.Mtime = mtime\n\t// 上传成功清理进度\n\tbase.SaveUploadProgress(d, nil, d.AccessToken, contentMd5)\n\treturn fileToObj(newFile), nil\n}\n\n// precreate 执行预上传操作，支持首次上传和 uploadid 过期重试\nfunc (d *BaiduNetdisk) precreate(ctx context.Context, path string, streamSize int64, blockListStr, contentMd5, sliceMd5 string, ctime, mtime int64) (*PrecreateResp, error) {\n\tparams := map[string]string{\"method\": \"precreate\"}\n\tform := map[string]string{\n\t\t\"path\":       path,\n\t\t\"size\":       strconv.FormatInt(streamSize, 10),\n\t\t\"isdir\":      \"0\",\n\t\t\"autoinit\":   \"1\",\n\t\t\"rtype\":      \"3\",\n\t\t\"block_list\": blockListStr,\n\t}\n\n\t// 只有在首次上传时才包含 content-md5 和 slice-md5\n\tif contentMd5 != \"\" && sliceMd5 != \"\" {\n\t\tform[\"content-md5\"] = contentMd5\n\t\tform[\"slice-md5\"] = sliceMd5\n\t}\n\n\tjoinTime(form, ctime, mtime)\n\n\tvar precreateResp PrecreateResp\n\t_, err := d.postForm(\"/xpan/file\", params, form, &precreateResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 修复时间，具体原因见 Put 方法注释的 **注意**\n\tif precreateResp.ReturnType == 2 {\n\t\tprecreateResp.File.Ctime = ctime\n\t\tprecreateResp.File.Mtime = mtime\n\t}\n\n\treturn &precreateResp, nil\n}\n\nfunc (d *BaiduNetdisk) uploadSlice(ctx context.Context, uploadUrl string, params map[string]string, fileName string, file *io.SectionReader) error {\n\tb := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))\n\tmw := multipart.NewWriter(b)\n\t_, err := mw.CreateFormFile(\"file\", fileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\theadSize := b.Len()\n\terr = mw.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\thead := bytes.NewReader(b.Bytes()[:headSize])\n\ttail := bytes.NewReader(b.Bytes()[headSize:])\n\trateLimitedRd := driver.NewLimitedUploadStream(ctx, io.MultiReader(head, file, tail))\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl+\"/rest/2.0/pcs/superfile2\", rateLimitedRd)\n\tif err != nil {\n\t\treturn err\n\t}\n\tquery := req.URL.Query()\n\tfor k, v := range params {\n\t\tquery.Set(k, v)\n\t}\n\treq.URL.RawQuery = query.Encode()\n\treq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\treq.ContentLength = int64(b.Len()) + file.Size()\n\n\tclient := net.NewHttpClient()\n\tif d.UploadSliceTimeout > 0 {\n\t\tclient.Timeout = time.Second * time.Duration(d.UploadSliceTimeout)\n\t} else {\n\t\tclient.Timeout = DEFAULT_UPLOAD_SLICE_TIMEOUT\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tb.Reset()\n\t_, err = b.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbody := b.Bytes()\n\trespStr := string(body)\n\tlog.Debugln(respStr)\n\tlower := strings.ToLower(respStr)\n\t// 合并 uploadid 过期检测逻辑\n\tif strings.Contains(lower, \"uploadid\") &&\n\t\t(strings.Contains(lower, \"invalid\") || strings.Contains(lower, \"expired\") || strings.Contains(lower, \"not found\")) {\n\t\treturn ErrUploadIDExpired\n\t}\n\n\terrCode := utils.Json.Get(body, \"error_code\").ToInt()\n\terrNo := utils.Json.Get(body, \"errno\").ToInt()\n\tif errCode != 0 || errNo != 0 {\n\t\treturn errs.NewErr(errs.StreamIncomplete, \"error uploading to baidu, response=%s\", respStr)\n\t}\n\treturn nil\n}\n\nfunc (d *BaiduNetdisk) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tdu, err := d.quota(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{DiskUsage: du}, nil\n}\n\nvar _ driver.Driver = (*BaiduNetdisk)(nil)\n"
  },
  {
    "path": "drivers/baidu_netdisk/meta.go",
    "content": "package baidu_netdisk\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tOrderBy               string `json:\"order_by\" type:\"select\" options:\"name,time,size\" default:\"name\"`\n\tOrderDirection        string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tDownloadAPI           string `json:\"download_api\" type:\"select\" options:\"official,crack,crack_video\" default:\"official\"`\n\tUseOnlineAPI          bool   `json:\"use_online_api\" default:\"true\"`\n\tAPIAddress            string `json:\"api_url_address\" default:\"https://api.oplist.org/baiduyun/renewapi\"`\n\tClientID              string `json:\"client_id\"`\n\tClientSecret          string `json:\"client_secret\"`\n\tCustomCrackUA         string `json:\"custom_crack_ua\" required:\"true\" default:\"netdisk\"`\n\tAccessToken           string\n\tRefreshToken          string `json:\"refresh_token\" required:\"true\"`\n\tUploadThread          string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\n\tUploadSliceTimeout    int    `json:\"upload_timeout\" type:\"number\" default:\"60\" help:\"per-slice upload timeout in seconds\"`\n\tUploadAPI             string `json:\"upload_api\" default:\"https://d.pcs.baidu.com\"`\n\tUseDynamicUploadAPI   bool   `json:\"use_dynamic_upload_api\" default:\"true\" help:\"dynamically get upload api domain, when enabled, the 'Upload API' setting will be used as a fallback if failed to get\"`\n\tCustomUploadPartSize  int64  `json:\"custom_upload_part_size\" type:\"number\" default:\"0\" help:\"0 for auto\"`\n\tLowBandwithUploadMode bool   `json:\"low_bandwith_upload_mode\" default:\"false\"`\n\tOnlyListVideoFile     bool   `json:\"only_list_video_file\" default:\"false\"`\n}\n\nconst (\n\tUPLOAD_FALLBACK_API          = \"https://d.pcs.baidu.com\" // 备用上传地址\n\tUPLOAD_URL_EXPIRE_TIME       = time.Minute * 60          // 上传地址有效期(分钟)\n\tDEFAULT_UPLOAD_SLICE_TIMEOUT = time.Second * 60          // 上传分片请求默认超时时间\n\tUPLOAD_RETRY_COUNT           = 3\n\tUPLOAD_RETRY_WAIT_TIME       = time.Second * 1\n\tUPLOAD_RETRY_MAX_WAIT_TIME   = time.Second * 5\n)\n\nvar config = driver.Config{\n\tName:        \"BaiduNetdisk\",\n\tDefaultRoot: \"/\",\n\tPreferProxy: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &BaiduNetdisk{}\n\t})\n}\n"
  },
  {
    "path": "drivers/baidu_netdisk/types.go",
    "content": "package baidu_netdisk\n\nimport (\n\t\"errors\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nvar (\n\tErrBaiduEmptyFilesNotAllowed = errors.New(\"empty files are not allowed by baidu netdisk\")\n)\n\ntype TokenErrResp struct {\n\tErrorDescription string `json:\"error_description\"`\n\tError            string `json:\"error\"`\n}\n\ntype File struct {\n\t//TkbindId     int    `json:\"tkbind_id\"`\n\t//OwnerType    int    `json:\"owner_type\"`\n\tCategory int `json:\"category\"`\n\t//RealCategory string `json:\"real_category\"`\n\tFsId int64 `json:\"fs_id\"`\n\t//OperId      int   `json:\"oper_id\"`\n\tThumbs struct {\n\t\t//Icon string `json:\"icon\"`\n\t\tUrl3 string `json:\"url3\"`\n\t\t//Url2 string `json:\"url2\"`\n\t\t//Url1 string `json:\"url1\"`\n\t} `json:\"thumbs\"`\n\t//Wpfile         int    `json:\"wpfile\"`\n\n\tSize int64 `json:\"size\"`\n\t//ExtentTinyint7 int    `json:\"extent_tinyint7\"`\n\tPath string `json:\"path\"`\n\t//Share          int    `json:\"share\"`\n\t//Pl             int    `json:\"pl\"`\n\tServerFilename string `json:\"server_filename\"`\n\tMd5            string `json:\"md5\"`\n\t//OwnerId        int    `json:\"owner_id\"`\n\t//Unlist int `json:\"unlist\"`\n\tIsdir int `json:\"isdir\"`\n\n\t// list resp\n\tServerCtime int64 `json:\"server_ctime\"`\n\tServerMtime int64 `json:\"server_mtime\"`\n\tLocalMtime  int64 `json:\"local_mtime\"`\n\tLocalCtime  int64 `json:\"local_ctime\"`\n\t//ServerAtime    int64    `json:\"server_atime\"` `\n\n\t// only create and precreate resp\n\tCtime int64 `json:\"ctime\"`\n\tMtime int64 `json:\"mtime\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\tif f.ServerFilename == \"\" {\n\t\tf.ServerFilename = path.Base(f.Path)\n\t}\n\tif f.ServerCtime == 0 {\n\t\tf.ServerCtime = f.Ctime\n\t}\n\tif f.ServerMtime == 0 {\n\t\tf.ServerMtime = f.Mtime\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       strconv.FormatInt(f.FsId, 10),\n\t\t\tPath:     f.Path,\n\t\t\tName:     f.ServerFilename,\n\t\t\tSize:     f.Size,\n\t\t\tModified: time.Unix(f.ServerMtime, 0),\n\t\t\tCtime:    time.Unix(f.ServerCtime, 0),\n\t\t\tIsFolder: f.Isdir == 1,\n\t\t\t// 百度API返回的MD5不可信，不使用HashInfo\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},\n\t}\n}\n\ntype ListResp struct {\n\tErrno     int    `json:\"errno\"`\n\tGuidInfo  string `json:\"guid_info\"`\n\tList      []File `json:\"list\"`\n\tRequestId int64  `json:\"request_id\"`\n\tGuid      int    `json:\"guid\"`\n}\n\ntype DownloadResp struct {\n\tErrmsg string `json:\"errmsg\"`\n\tErrno  int    `json:\"errno\"`\n\tList   []struct {\n\t\t//Category    int    `json:\"category\"`\n\t\t//DateTaken   int    `json:\"date_taken,omitempty\"`\n\t\tDlink string `json:\"dlink\"`\n\t\t//Filename    string `json:\"filename\"`\n\t\t//FsId        int64  `json:\"fs_id\"`\n\t\t//Height      int    `json:\"height,omitempty\"`\n\t\t//Isdir       int    `json:\"isdir\"`\n\t\t//Md5         string `json:\"md5\"`\n\t\t//OperId      int    `json:\"oper_id\"`\n\t\t//Path        string `json:\"path\"`\n\t\t//ServerCtime int    `json:\"server_ctime\"`\n\t\t//ServerMtime int    `json:\"server_mtime\"`\n\t\t//Size        int    `json:\"size\"`\n\t\t//Thumbs      struct {\n\t\t//\tIcon string `json:\"icon,omitempty\"`\n\t\t//\tUrl1 string `json:\"url1,omitempty\"`\n\t\t//\tUrl2 string `json:\"url2,omitempty\"`\n\t\t//\tUrl3 string `json:\"url3,omitempty\"`\n\t\t//} `json:\"thumbs\"`\n\t\t//Width int `json:\"width,omitempty\"`\n\t} `json:\"list\"`\n\t//Names struct {\n\t//} `json:\"names\"`\n\tRequestId string `json:\"request_id\"`\n}\n\ntype DownloadResp2 struct {\n\tErrno int `json:\"errno\"`\n\tInfo  []struct {\n\t\t//ExtentTinyint4 int `json:\"extent_tinyint4\"`\n\t\t//ExtentTinyint1 int `json:\"extent_tinyint1\"`\n\t\t//Bitmap string `json:\"bitmap\"`\n\t\t//Category int `json:\"category\"`\n\t\t//Isdir int `json:\"isdir\"`\n\t\t//Videotag int `json:\"videotag\"`\n\t\tDlink string `json:\"dlink\"`\n\t\t//OperID int64 `json:\"oper_id\"`\n\t\t//PathMd5 int `json:\"path_md5\"`\n\t\t//Wpfile int `json:\"wpfile\"`\n\t\t//LocalMtime int `json:\"local_mtime\"`\n\t\t/*Thumbs struct {\n\t\t\tIcon string `json:\"icon\"`\n\t\t\tURL3 string `json:\"url3\"`\n\t\t\tURL2 string `json:\"url2\"`\n\t\t\tURL1 string `json:\"url1\"`\n\t\t} `json:\"thumbs\"`*/\n\t\t//PlaySource int `json:\"play_source\"`\n\t\t//Share int `json:\"share\"`\n\t\t//FileKey string `json:\"file_key\"`\n\t\t//Errno int `json:\"errno\"`\n\t\t//LocalCtime int `json:\"local_ctime\"`\n\t\t//Rotate int `json:\"rotate\"`\n\t\t//Metadata time.Time `json:\"metadata\"`\n\t\t//Height int `json:\"height\"`\n\t\t//SampleRate int `json:\"sample_rate\"`\n\t\t//Width int `json:\"width\"`\n\t\t//OwnerType int `json:\"owner_type\"`\n\t\t//Privacy int `json:\"privacy\"`\n\t\t//ExtentInt3 int64 `json:\"extent_int3\"`\n\t\t//RealCategory string `json:\"real_category\"`\n\t\t//SrcLocation string `json:\"src_location\"`\n\t\t//MetaInfo string `json:\"meta_info\"`\n\t\t//ID string `json:\"id\"`\n\t\t//Duration int `json:\"duration\"`\n\t\t//FileSize string `json:\"file_size\"`\n\t\t//Channels int `json:\"channels\"`\n\t\t//UseSegment int `json:\"use_segment\"`\n\t\t//ServerCtime int `json:\"server_ctime\"`\n\t\t//Resolution string `json:\"resolution\"`\n\t\t//OwnerID int `json:\"owner_id\"`\n\t\t//ExtraInfo string `json:\"extra_info\"`\n\t\t//Size int `json:\"size\"`\n\t\t//FsID int64 `json:\"fs_id\"`\n\t\t//ExtentTinyint3 int `json:\"extent_tinyint3\"`\n\t\t//Md5 string `json:\"md5\"`\n\t\t//Path string `json:\"path\"`\n\t\t//FrameRate int `json:\"frame_rate\"`\n\t\t//ExtentTinyint2 int `json:\"extent_tinyint2\"`\n\t\t//ServerFilename string `json:\"server_filename\"`\n\t\t//ServerMtime int `json:\"server_mtime\"`\n\t\t//TkbindID int `json:\"tkbind_id\"`\n\t} `json:\"info\"`\n\tRequestID int64 `json:\"request_id\"`\n}\n\ntype PrecreateResp struct {\n\tErrno      int   `json:\"errno\"`\n\tRequestId  int64 `json:\"request_id\"`\n\tReturnType int   `json:\"return_type\"`\n\n\t// return_type=1\n\tPath      string `json:\"path\"`\n\tUploadid  string `json:\"uploadid\"`\n\tBlockList []int  `json:\"block_list\"`\n\n\t// return_type=2\n\tFile File `json:\"info\"`\n\n\tUploadURL string `json:\"-\"` // 保存断点续传对应的上传域名\n}\n\ntype UploadServerResp struct {\n\tBakServer  []any `json:\"bak_server\"`\n\tBakServers []struct {\n\t\tServer string `json:\"server\"`\n\t} `json:\"bak_servers\"`\n\tClientIP    string `json:\"client_ip\"`\n\tErrorCode   int    `json:\"error_code\"`\n\tErrorMsg    string `json:\"error_msg\"`\n\tExpire      int    `json:\"expire\"`\n\tHost        string `json:\"host\"`\n\tNewno       string `json:\"newno\"`\n\tQuicServer  []any  `json:\"quic_server\"`\n\tQuicServers []struct {\n\t\tServer string `json:\"server\"`\n\t} `json:\"quic_servers\"`\n\tRequestID  int64 `json:\"request_id\"`\n\tServer     []any `json:\"server\"`\n\tServerTime int   `json:\"server_time\"`\n\tServers    []struct {\n\t\tServer string `json:\"server\"`\n\t} `json:\"servers\"`\n\tSl int `json:\"sl\"`\n}\n\ntype QuotaResp struct {\n\tErrno     int   `json:\"errno\"`\n\tRequestId int64 `json:\"request_id\"`\n\tTotal     int64 `json:\"total\"`\n\tUsed      int64 `json:\"used\"`\n\t//FreeSpace      uint64 `json:\"free\"`\n\t//Expire    bool   `json:\"expire\"`\n}\n"
  },
  {
    "path": "drivers/baidu_netdisk/util.go",
    "content": "package baidu_netdisk\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *BaiduNetdisk) refreshToken() error {\n\terr := d._refreshToken()\n\tif err != nil && errors.Is(err, errs.EmptyToken) {\n\t\terr = d._refreshToken()\n\t}\n\treturn err\n}\n\nfunc (d *BaiduNetdisk) _refreshToken() error {\n\t// 使用在线API刷新Token，无需ClientID和ClientSecret\n\tif d.UseOnlineAPI && len(d.APIAddress) > 0 {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"baiduyun_go\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\td.RefreshToken = resp.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\t// 使用本地客户端的情况下检查是否为空\n\tif d.ClientID == \"\" || d.ClientSecret == \"\" {\n\t\treturn fmt.Errorf(\"empty ClientID or ClientSecret\")\n\t}\n\t// 走原有的刷新逻辑\n\tu := \"https://openapi.baidu.com/oauth/2.0/token\"\n\tvar resp base.TokenResp\n\tvar e TokenErrResp\n\t_, err := base.RestyClient.R().\n\t\tSetResult(&resp).\n\t\tSetError(&e).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t\t\"client_id\":     d.ClientID,\n\t\t\t\"client_secret\": d.ClientSecret,\n\t\t}).\n\t\tGet(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(\"%s : %s\", e.Error, e.ErrorDescription)\n\t}\n\tif resp.RefreshToken == \"\" {\n\t\treturn errs.EmptyToken\n\t}\n\td.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tvar result []byte\n\terr := retry.Do(func() error {\n\t\treq := base.RestyClient.R()\n\t\treq.SetQueryParam(\"access_token\", d.AccessToken)\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t\tif resp != nil {\n\t\t\treq.SetResult(resp)\n\t\t}\n\t\tres, err := req.Execute(method, furl)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debugf(\"[baidu_netdisk] req: %s, resp: %s\", furl, res.String())\n\t\terrno := utils.Json.Get(res.Body(), \"errno\").ToInt()\n\t\tif errno != 0 {\n\t\t\tif utils.SliceContains([]int{111, -6}, errno) {\n\t\t\t\tlog.Info(\"[baidu_netdisk] refreshing baidu_netdisk token.\")\n\t\t\t\terr2 := d.refreshToken()\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn retry.Unrecoverable(err2)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif errno == 31023 && d.DownloadAPI == \"crack_video\" {\n\t\t\t\tresult = res.Body()\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/\", furl, errno)\n\t\t}\n\t\tresult = res.Body()\n\t\treturn nil\n\t},\n\t\tretry.LastErrorOnly(true),\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\treturn result, err\n}\n\nfunc (d *BaiduNetdisk) get(pathname string, params map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.request(\"https://pan.baidu.com/rest/2.0\"+pathname, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(params)\n\t}, resp)\n}\n\nfunc (d *BaiduNetdisk) postForm(pathname string, params map[string]string, form map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.request(\"https://pan.baidu.com/rest/2.0\"+pathname, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetQueryParams(params)\n\t\treq.SetFormData(form)\n\t}, resp)\n}\n\nfunc (d *BaiduNetdisk) getFiles(dir string) ([]File, error) {\n\tstart := 0\n\tlimit := 1000\n\tparams := map[string]string{\n\t\t\"method\": \"list\",\n\t\t\"dir\":    dir,\n\t\t\"web\":    \"web\",\n\t}\n\tif d.OrderBy != \"\" {\n\t\tparams[\"order\"] = d.OrderBy\n\t\tif d.OrderDirection == \"desc\" {\n\t\t\tparams[\"desc\"] = \"1\"\n\t\t}\n\t}\n\tres := make([]File, 0)\n\tfor {\n\t\tparams[\"start\"] = strconv.Itoa(start)\n\t\tparams[\"limit\"] = strconv.Itoa(limit)\n\t\tvar resp ListResp\n\t\t_, err := d.get(\"/xpan/file\", params, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(resp.List) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif d.OnlyListVideoFile {\n\t\t\tfor _, file := range resp.List {\n\t\t\t\tif file.Isdir == 1 || file.Category == 1 {\n\t\t\t\t\tres = append(res, file)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tres = append(res, resp.List...)\n\t\t}\n\n\t\tif len(resp.List) < limit {\n\t\t\tbreak\n\t\t}\n\t\tstart += limit\n\t}\n\treturn res, nil\n}\n\nfunc (d *BaiduNetdisk) linkOfficial(file model.Obj, _ model.LinkArgs) (*model.Link, error) {\n\tvar resp DownloadResp\n\tparams := map[string]string{\n\t\t\"method\": \"filemetas\",\n\t\t\"fsids\":  fmt.Sprintf(\"[%s]\", file.GetID()),\n\t\t\"dlink\":  \"1\",\n\t}\n\t_, err := d.get(\"/xpan/multimedia\", params, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu := fmt.Sprintf(\"%s&access_token=%s\", resp.List[0].Dlink, d.AccessToken)\n\tres, err := base.NoRedirectClient.R().SetHeader(\"User-Agent\", \"pan.baidu.com\").Head(u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// if res.StatusCode() == 302 {\n\tu = res.Header().Get(\"location\")\n\t//}\n\n\treturn &model.Link{\n\t\tURL: u,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{\"pan.baidu.com\"},\n\t\t},\n\t}, nil\n}\n\nfunc (d *BaiduNetdisk) linkCrack(file model.Obj, _ model.LinkArgs) (*model.Link, error) {\n\tvar resp DownloadResp2\n\tparam := map[string]string{\n\t\t\"target\": fmt.Sprintf(\"[\\\"%s\\\"]\", file.GetPath()),\n\t\t\"dlink\":  \"1\",\n\t\t\"web\":    \"5\",\n\t\t\"origin\": \"dlna\",\n\t}\n\t_, err := d.request(\"https://pan.baidu.com/api/filemetas\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(param)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL: resp.Info[0].Dlink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{d.CustomCrackUA},\n\t\t},\n\t}, nil\n}\n\nfunc (d *BaiduNetdisk) linkCrackVideo(file model.Obj, _ model.LinkArgs) (*model.Link, error) {\n\tparam := map[string]string{\n\t\t\"type\":       \"VideoURL\",\n\t\t\"path\":       file.GetPath(),\n\t\t\"fs_id\":      file.GetID(),\n\t\t\"devuid\":     \"0%1\",\n\t\t\"clienttype\": \"1\",\n\t\t\"channel\":    \"android_15_25010PN30C_bd-netdisk_1523a\",\n\t\t\"nom3u8\":     \"1\",\n\t\t\"dlink\":      \"1\",\n\t\t\"media\":      \"1\",\n\t\t\"origin\":     \"dlna\",\n\t}\n\tresp, err := d.request(\"https://pan.baidu.com/api/mediainfo\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(param)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL: utils.Json.Get(resp, \"info\", \"dlink\").ToString(),\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{d.CustomCrackUA},\n\t\t},\n\t}, nil\n}\n\nfunc (d *BaiduNetdisk) manage(opera string, filelist any) ([]byte, error) {\n\tparams := map[string]string{\n\t\t\"method\": \"filemanager\",\n\t\t\"opera\":  opera,\n\t}\n\tmarshal, _ := utils.Json.MarshalToString(filelist)\n\treturn d.postForm(\"/xpan/file\", params, map[string]string{\n\t\t\"async\":    \"0\",\n\t\t\"filelist\": marshal,\n\t\t\"ondup\":    \"fail\",\n\t}, nil)\n}\n\nfunc (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string, resp any, mtime, ctime int64) ([]byte, error) {\n\tparams := map[string]string{\n\t\t\"method\": \"create\",\n\t}\n\tform := map[string]string{\n\t\t\"path\":  path,\n\t\t\"size\":  strconv.FormatInt(size, 10),\n\t\t\"isdir\": strconv.Itoa(isdir),\n\t\t\"rtype\": \"3\",\n\t}\n\tif mtime != 0 && ctime != 0 {\n\t\tjoinTime(form, ctime, mtime)\n\t}\n\n\tif uploadid != \"\" {\n\t\tform[\"uploadid\"] = uploadid\n\t}\n\tif block_list != \"\" {\n\t\tform[\"block_list\"] = block_list\n\t}\n\treturn d.postForm(\"/xpan/file\", params, form, resp)\n}\n\nfunc joinTime(form map[string]string, ctime, mtime int64) {\n\tform[\"local_mtime\"] = strconv.FormatInt(mtime, 10)\n\tform[\"local_ctime\"] = strconv.FormatInt(ctime, 10)\n}\n\nconst (\n\tDefaultSliceSize int64 = 4 * utils.MB\n\tVipSliceSize     int64 = 16 * utils.MB\n\tSVipSliceSize    int64 = 32 * utils.MB\n\n\tMaxSliceNum       = 2048 // 文档写的是 1024/没写 ，但实际测试是 2048\n\tSliceStep   int64 = 1 * utils.MB\n)\n\nfunc (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {\n\t// 非会员固定为 4MB\n\tif d.vipType == 0 {\n\t\tif d.CustomUploadPartSize != 0 {\n\t\t\tlog.Warnf(\"[baidu_netdisk] CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize\")\n\t\t}\n\t\tif filesize > MaxSliceNum*DefaultSliceSize {\n\t\t\tlog.Warnf(\"[baidu_netdisk] File size(%d) is too large, may cause upload failure\", filesize)\n\t\t}\n\n\t\treturn DefaultSliceSize\n\t}\n\n\tif d.CustomUploadPartSize != 0 {\n\t\tif d.CustomUploadPartSize < DefaultSliceSize {\n\t\t\tlog.Warnf(\"[baidu_netdisk] CustomUploadPartSize(%d) is less than DefaultSliceSize(%d), use DefaultSliceSize\", d.CustomUploadPartSize, DefaultSliceSize)\n\t\t\treturn DefaultSliceSize\n\t\t}\n\n\t\tif d.vipType == 1 && d.CustomUploadPartSize > VipSliceSize {\n\t\t\tlog.Warnf(\"[baidu_netdisk] CustomUploadPartSize(%d) is greater than VipSliceSize(%d), use VipSliceSize\", d.CustomUploadPartSize, VipSliceSize)\n\t\t\treturn VipSliceSize\n\t\t}\n\n\t\tif d.vipType == 2 && d.CustomUploadPartSize > SVipSliceSize {\n\t\t\tlog.Warnf(\"[baidu_netdisk] CustomUploadPartSize(%d) is greater than SVipSliceSize(%d), use SVipSliceSize\", d.CustomUploadPartSize, SVipSliceSize)\n\t\t\treturn SVipSliceSize\n\t\t}\n\n\t\treturn d.CustomUploadPartSize\n\t}\n\n\tmaxSliceSize := DefaultSliceSize\n\n\tswitch d.vipType {\n\tcase 1:\n\t\tmaxSliceSize = VipSliceSize\n\tcase 2:\n\t\tmaxSliceSize = SVipSliceSize\n\t}\n\n\t// upload on low bandwidth\n\tif d.LowBandwithUploadMode {\n\t\tsize := DefaultSliceSize\n\n\t\tfor size <= maxSliceSize {\n\t\t\tif filesize <= MaxSliceNum*size {\n\t\t\t\treturn size\n\t\t\t}\n\n\t\t\tsize += SliceStep\n\t\t}\n\t}\n\n\tif filesize > MaxSliceNum*maxSliceSize {\n\t\tlog.Warnf(\"[baidu_netdisk] File size(%d) is too large, may cause upload failure\", filesize)\n\t}\n\n\treturn maxSliceSize\n}\n\nfunc (d *BaiduNetdisk) quota(ctx context.Context) (model.DiskUsage, error) {\n\tvar resp QuotaResp\n\t_, err := d.request(\"https://pan.baidu.com/api/quota\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn model.DiskUsage{}, err\n\t}\n\treturn model.DiskUsage{TotalSpace: resp.Total, UsedSpace: resp.Used}, nil\n}\n\n// getUploadUrl 从开放平台获取上传域名/地址，并发请求会被合并，结果会在 uploadid 生命周期内复用。\n// 如果获取失败，则返回 Upload API设置项。\nfunc (d *BaiduNetdisk) getUploadUrl(path, uploadId string) string {\n\tif !d.UseDynamicUploadAPI || uploadId == \"\" {\n\t\treturn d.UploadAPI\n\t}\n\n\tuploadUrl, err := d.requestForUploadUrl(path, uploadId)\n\tif err != nil {\n\t\treturn d.UploadAPI\n\t}\n\treturn uploadUrl\n}\n\n// requestForUploadUrl 请求获取上传地址。\n// 实测此接口不需要认证，传method和upload_version就行，不过还是按文档规范调用。\n// https://pan.baidu.com/union/doc/Mlvw5hfnr\nfunc (d *BaiduNetdisk) requestForUploadUrl(path, uploadId string) (string, error) {\n\tparams := map[string]string{\n\t\t\"method\":         \"locateupload\",\n\t\t\"appid\":          \"250528\",\n\t\t\"path\":           path,\n\t\t\"uploadid\":       uploadId,\n\t\t\"upload_version\": \"2.0\",\n\t}\n\tapiUrl := \"https://d.pcs.baidu.com/rest/2.0/pcs/file\"\n\tvar resp UploadServerResp\n\t_, err := d.request(apiUrl, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(params)\n\t}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// 应该是https开头的一个地址\n\tvar uploadUrl string\n\tif len(resp.Servers) > 0 {\n\t\tuploadUrl = resp.Servers[0].Server\n\t} else if len(resp.BakServers) > 0 {\n\t\tuploadUrl = resp.BakServers[0].Server\n\t}\n\tif uploadUrl == \"\" {\n\t\treturn \"\", errors.New(\"upload URL is empty\")\n\t}\n\treturn uploadUrl, nil\n}\n\n// func encodeURIComponent(str string) string {\n// \tr := url.QueryEscape(str)\n// \tr = strings.ReplaceAll(r, \"+\", \"%20\")\n// \treturn r\n// }\n\nfunc DecryptMd5(encryptMd5 string) string {\n\tif _, err := hex.DecodeString(encryptMd5); err == nil {\n\t\treturn encryptMd5\n\t}\n\n\tvar out strings.Builder\n\tout.Grow(len(encryptMd5))\n\tfor i, n := 0, int64(0); i < len(encryptMd5); i++ {\n\t\tif i == 9 {\n\t\t\tn = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')\n\t\t} else {\n\t\t\tn, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)\n\t\t}\n\t\tout.WriteString(strconv.FormatInt(n^int64(15&i), 16))\n\t}\n\n\tencryptMd5 = out.String()\n\treturn encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]\n}\n\nfunc EncryptMd5(originalMd5 string) string {\n\treversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]\n\n\tvar out strings.Builder\n\tout.Grow(len(reversed))\n\tfor i, n := 0, int64(0); i < len(reversed); i++ {\n\t\tn, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)\n\t\tn ^= int64(15 & i)\n\t\tif i == 9 {\n\t\t\tout.WriteRune(rune(n) + 'g')\n\t\t} else {\n\t\t\tout.WriteString(strconv.FormatInt(n, 16))\n\t\t}\n\t}\n\treturn out.String()\n}\n"
  },
  {
    "path": "drivers/baidu_photo/driver.go",
    "content": "package baiduphoto\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype BaiduPhoto struct {\n\tmodel.Storage\n\tAddition\n\n\t// AccessToken string\n\tUk       int64\n\tbdstoken string\n\troot     model.Obj\n\n\tuploadThread int\n}\n\nfunc (d *BaiduPhoto) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *BaiduPhoto) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *BaiduPhoto) Init(ctx context.Context) error {\n\td.uploadThread, _ = strconv.Atoi(d.UploadThread)\n\tif d.uploadThread < 1 || d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 3, \"3\"\n\t}\n\n\t// if err := d.refreshToken(); err != nil {\n\t// \treturn err\n\t// }\n\n\t// root\n\tif d.AlbumID != \"\" {\n\t\talbumID := strings.Split(d.AlbumID, \"|\")[0]\n\t\talbum, err := d.GetAlbumDetail(ctx, albumID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.root = album\n\t} else {\n\t\td.root = &Root{\n\t\t\tName:     \"root\",\n\t\t\tModified: d.Modified,\n\t\t\tIsFolder: true,\n\t\t}\n\t}\n\n\t// uk\n\tinfo, err := d.uInfo()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.bdstoken, err = d.getBDStoken()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Uk, err = strconv.ParseInt(info.YouaID, 10, 64)\n\treturn err\n}\n\nfunc (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {\n\treturn d.root, nil\n}\n\nfunc (d *BaiduPhoto) Drop(ctx context.Context) error {\n\t// d.AccessToken = \"\"\n\td.Uk = 0\n\td.root = nil\n\treturn nil\n}\n\nfunc (d *BaiduPhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar err error\n\n\t/* album */\n\tif album, ok := dir.(*Album); ok {\n\t\tvar files []AlbumFile\n\t\tfiles, err = d.GetAllAlbumFile(ctx, album, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn utils.MustSliceConvert(files, func(file AlbumFile) model.Obj {\n\t\t\treturn &file\n\t\t}), nil\n\t}\n\n\t/* root */\n\tvar albums []Album\n\tif d.ShowType != \"root_only_file\" {\n\t\talbums, err = d.GetAllAlbum(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar files []File\n\tif d.ShowType != \"root_only_album\" {\n\t\tfiles, err = d.GetAllFile(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn append(\n\t\tutils.MustSliceConvert(albums, func(album Album) model.Obj {\n\t\t\treturn &album\n\t\t}),\n\t\tutils.MustSliceConvert(files, func(album File) model.Obj {\n\t\t\treturn &album\n\t\t})...,\n\t), nil\n\n}\n\nfunc (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tswitch file := file.(type) {\n\tcase *File:\n\t\treturn d.linkFile(ctx, file, args)\n\tcase *AlbumFile:\n\t\t// 处理共享相册\n\t\tif d.Uk != file.Uk {\n\t\t\t// 有概率无法获取到链接\n\t\t\t// return d.linkAlbum(ctx, file, args)\n\n\t\t\tf, err := d.CopyAlbumFile(ctx, file)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.linkFile(ctx, f, args)\n\t\t}\n\t\treturn d.linkFile(ctx, &file.File, args)\n\t}\n\treturn nil, errs.NotFile\n}\n\nvar joinReg = regexp.MustCompile(`(?i)join:([\\S]*)`)\n\nfunc (d *BaiduPhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif _, ok := parentDir.(*Root); ok {\n\t\tcode := joinReg.FindStringSubmatch(dirName)\n\t\tif len(code) > 1 {\n\t\t\treturn d.JoinAlbum(ctx, code[1])\n\t\t}\n\t\treturn d.CreateAlbum(ctx, dirName)\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *BaiduPhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tswitch file := srcObj.(type) {\n\tcase *File:\n\t\tif album, ok := dstDir.(*Album); ok {\n\t\t\t//rootfile ->  album\n\t\t\treturn d.AddAlbumFile(ctx, album, file)\n\t\t}\n\tcase *AlbumFile:\n\t\tswitch album := dstDir.(type) {\n\t\tcase *Root:\n\t\t\t//albumfile -> root\n\t\t\treturn d.CopyAlbumFile(ctx, file)\n\t\tcase *Album:\n\t\t\t// albumfile -> root -> album\n\t\t\trootfile, err := d.CopyAlbumFile(ctx, file)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.AddAlbumFile(ctx, album, rootfile)\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *BaiduPhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif file, ok := srcObj.(*AlbumFile); ok {\n\t\tswitch dstDir.(type) {\n\t\tcase *Album, *Root: // albumfile -> root -> album or albumfile -> root\n\t\t\tnewObj, err := d.Copy(ctx, srcObj, dstDir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// 删除原相册文件\n\t\t\t_ = d.DeleteAlbumFile(ctx, file)\n\t\t\treturn newObj, nil\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *BaiduPhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\t// 仅支持相册改名\n\tif album, ok := srcObj.(*Album); ok {\n\t\treturn d.SetAlbumName(ctx, album, newName)\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *BaiduPhoto) Remove(ctx context.Context, obj model.Obj) error {\n\tswitch obj := obj.(type) {\n\tcase *File:\n\t\treturn d.DeleteFile(ctx, obj)\n\tcase *AlbumFile:\n\t\treturn d.DeleteAlbumFile(ctx, obj)\n\tcase *Album:\n\t\treturn d.DeleteAlbum(ctx, obj)\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// 不支持大小为0的文件\n\tif stream.GetSize() == 0 {\n\t\treturn nil, fmt.Errorf(\"file size cannot be zero\")\n\t}\n\n\t// TODO:\n\t// 暂时没有找到妙传方式\n\tvar (\n\t\tcache = stream.GetFile()\n\t\ttmpF  *os.File\n\t\terr   error\n\t)\n\tif _, ok := cache.(io.ReaderAt); !ok {\n\t\ttmpF, err = os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = tmpF.Close()\n\t\t\t_ = os.Remove(tmpF.Name())\n\t\t}()\n\t\tcache = tmpF\n\t}\n\n\tconst DEFAULT int64 = 1 << 22\n\tconst SliceSize int64 = 1 << 18\n\n\t// 计算需要的数据\n\tstreamSize := stream.GetSize()\n\tcount := 1\n\tif streamSize > DEFAULT {\n\t\tcount = int((streamSize + DEFAULT - 1) / DEFAULT)\n\t}\n\tlastBlockSize := streamSize % DEFAULT\n\tif lastBlockSize == 0 {\n\t\tlastBlockSize = DEFAULT\n\t}\n\n\t// step.1 计算MD5\n\tsliceMD5List := make([]string, 0, count)\n\tbyteSize := int64(DEFAULT)\n\tfileMd5H := md5.New()\n\tsliceMd5H := md5.New()\n\tsliceMd5H2 := md5.New()\n\tslicemd5H2Write := utils.LimitWriter(sliceMd5H2, SliceSize)\n\twriters := []io.Writer{fileMd5H, sliceMd5H, slicemd5H2Write}\n\tif tmpF != nil {\n\t\twriters = append(writers, tmpF)\n\t}\n\twritten := int64(0)\n\tfor i := 1; i <= count; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t\tif i == count {\n\t\t\tbyteSize = lastBlockSize\n\t\t}\n\t\tn, err := utils.CopyWithBufferN(io.MultiWriter(writers...), stream, byteSize)\n\t\twritten += n\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn nil, err\n\t\t}\n\t\tsliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5H.Sum(nil)))\n\t\tsliceMd5H.Reset()\n\t}\n\tif tmpF != nil {\n\t\tif written != streamSize {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, incoming stream actual size= %d, expect = %d \", written, streamSize)\n\t\t}\n\t\t_, err = tmpF.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, can't seek to 0 \")\n\t\t}\n\t}\n\tcontentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))\n\tsliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))\n\tblockListStr, _ := utils.Json.MarshalToString(sliceMD5List)\n\n\t// step.2 预上传\n\tparams := map[string]string{\n\t\t\"autoinit\":    \"1\",\n\t\t\"isdir\":       \"0\",\n\t\t\"rtype\":       \"1\",\n\t\t\"ctype\":       \"11\",\n\t\t\"path\":        fmt.Sprintf(\"/%s\", stream.GetName()),\n\t\t\"size\":        fmt.Sprint(streamSize),\n\t\t\"slice-md5\":   sliceMd5,\n\t\t\"content-md5\": contentMd5,\n\t\t\"block_list\":  blockListStr,\n\t}\n\n\t// 尝试获取之前的进度\n\tprecreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, strconv.FormatInt(d.Uk, 10), contentMd5)\n\tif !ok {\n\t\t_, err = d.Post(FILE_API_URL_V1+\"/precreate\", func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetFormData(params)\n\t\t\tr.SetQueryParam(\"bdstoken\", d.bdstoken)\n\t\t}, &precreateResp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tswitch precreateResp.ReturnType {\n\tcase 1: //step.3 上传文件切片\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,\n\t\t\tretry.Attempts(3),\n\t\t\tretry.Delay(time.Second),\n\t\t\tretry.DelayType(retry.BackOffDelay))\n\n\t\tfor i, partseq := range precreateResp.BlockList {\n\t\t\tif utils.IsCanceled(upCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ti, partseq, offset, byteSize := i, partseq, int64(partseq)*DEFAULT, DEFAULT\n\t\t\tif partseq+1 == count {\n\t\t\t\tbyteSize = lastBlockSize\n\t\t\t}\n\n\t\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t\tuploadParams := map[string]string{\n\t\t\t\t\t\"method\":   \"upload\",\n\t\t\t\t\t\"path\":     params[\"path\"],\n\t\t\t\t\t\"partseq\":  fmt.Sprint(partseq),\n\t\t\t\t\t\"uploadid\": precreateResp.UploadID,\n\t\t\t\t\t\"app_id\":   \"16051585\",\n\t\t\t\t}\n\t\t\t\t_, err = d.Post(\"https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2\", func(r *resty.Request) {\n\t\t\t\t\tr.SetContext(ctx)\n\t\t\t\t\tr.SetQueryParams(uploadParams)\n\t\t\t\t\tr.SetFileReader(\"file\", stream.GetName(),\n\t\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, io.NewSectionReader(cache, offset, byteSize)))\n\t\t\t\t}, nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tup(float64(threadG.Success()+1) * 100 / float64(len(precreateResp.BlockList)+1))\n\t\t\t\tprecreateResp.BlockList[i] = -1\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\tif err = threadG.Wait(); err != nil {\n\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\tprecreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })\n\t\t\t\tbase.SaveUploadProgress(d, strconv.FormatInt(d.Uk, 10), contentMd5)\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer up(100)\n\t\tfallthrough\n\tcase 2: //step.4 创建文件\n\t\tparams[\"uploadid\"] = precreateResp.UploadID\n\t\t_, err = d.Post(FILE_API_URL_V1+\"/create\", func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetFormData(params)\n\t\t\tr.SetQueryParam(\"bdstoken\", d.bdstoken)\n\t\t}, &precreateResp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfallthrough\n\tcase 3: //step.5 增加到相册\n\t\trootfile := precreateResp.Data.toFile()\n\t\tif album, ok := dstDir.(*Album); ok {\n\t\t\treturn d.AddAlbumFile(ctx, album, rootfile)\n\t\t}\n\t\treturn rootfile, nil\n\t}\n\treturn nil, errs.NotSupport\n}\n\nvar _ driver.Driver = (*BaiduPhoto)(nil)\nvar _ driver.GetRooter = (*BaiduPhoto)(nil)\nvar _ driver.MkdirResult = (*BaiduPhoto)(nil)\nvar _ driver.CopyResult = (*BaiduPhoto)(nil)\nvar _ driver.MoveResult = (*BaiduPhoto)(nil)\nvar _ driver.Remove = (*BaiduPhoto)(nil)\nvar _ driver.PutResult = (*BaiduPhoto)(nil)\nvar _ driver.RenameResult = (*BaiduPhoto)(nil)\n"
  },
  {
    "path": "drivers/baidu_photo/help.go",
    "content": "package baiduphoto\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// Tid生成\nfunc getTid() string {\n\treturn fmt.Sprintf(\"3%d%.0f\", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))\n}\n\nfunc toTime(t int64) *time.Time {\n\ttm := time.Unix(t, 0)\n\treturn &tm\n}\n\nfunc fsidsFormatNotUk(ids ...int64) string {\n\tbuf := utils.MustSliceConvert(ids, func(id int64) string {\n\t\treturn fmt.Sprintf(`{\"fsid\":%d}`, id)\n\t})\n\treturn fmt.Sprintf(\"[%s]\", strings.Join(buf, \",\"))\n}\n\nfunc getFileName(path string) string {\n\treturn path[strings.LastIndex(path, \"/\")+1:]\n}\n\nfunc MustString(str string, err error) string {\n\treturn str\n}\n\n/*\n*\t处理文件变化\n*\t最大程度利用重复数据\n**/\nfunc copyFile(file *AlbumFile, cf *CopyFile) *File {\n\treturn &File{\n\t\tFsid:     cf.Fsid,\n\t\tPath:     cf.Path,\n\t\tCtime:    cf.Ctime,\n\t\tMtime:    cf.Ctime,\n\t\tSize:     file.Size,\n\t\tThumburl: file.Thumburl,\n\t}\n}\n\nfunc moveFileToAlbumFile(file *File, album *Album, uk int64) *AlbumFile {\n\treturn &AlbumFile{\n\t\tFile:    *file,\n\t\tAlbumID: album.AlbumID,\n\t\tTid:     album.Tid,\n\t\tUk:      uk,\n\t}\n}\n\nfunc renameAlbum(album *Album, newName string) *Album {\n\treturn &Album{\n\t\tAlbumID:      album.AlbumID,\n\t\tTid:          album.Tid,\n\t\tJoinTime:     album.JoinTime,\n\t\tCreationTime: album.CreationTime,\n\t\tTitle:        newName,\n\t\tMtime:        time.Now().Unix(),\n\t}\n}\n\nfunc BoolToIntStr(b bool) string {\n\tif b {\n\t\treturn \"1\"\n\t}\n\treturn \"0\"\n}\n"
  },
  {
    "path": "drivers/baidu_photo/meta.go",
    "content": "package baiduphoto\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// RefreshToken string `json:\"refresh_token\" required:\"true\"`\n\tCookie   string `json:\"cookie\" required:\"true\"`\n\tShowType string `json:\"show_type\" type:\"select\" options:\"root,root_only_album,root_only_file\" default:\"root\"`\n\tAlbumID  string `json:\"album_id\"`\n\t//AlbumPassword string `json:\"album_password\"`\n\tDeleteOrigin bool `json:\"delete_origin\"`\n\t// ClientID     string `json:\"client_id\" required:\"true\" default:\"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v\"`\n\t// ClientSecret string `json:\"client_secret\" required:\"true\" default:\"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG\"`\n\tUploadThread string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\n}\n\nvar config = driver.Config{\n\tName:          \"BaiduPhoto\",\n\tLocalSort:     true,\n\tLinkCacheMode: driver.LinkCacheUA,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &BaiduPhoto{}\n\t})\n}\n"
  },
  {
    "path": "drivers/baidu_photo/types.go",
    "content": "package baiduphoto\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype TokenErrResp struct {\n\tErrorDescription string `json:\"error_description\"`\n\tErrorMsg         string `json:\"error\"`\n}\n\nfunc (e *TokenErrResp) Error() string {\n\treturn fmt.Sprint(e.ErrorMsg, \" : \", e.ErrorDescription)\n}\n\ntype Erron struct {\n\tErrno     int `json:\"errno\"`\n\tRequestID int `json:\"request_id\"`\n}\n\n// 用户信息\ntype UInfo struct {\n\t// uk\n\tYouaID string `json:\"youa_id\"`\n}\n\ntype Page struct {\n\tHasMore int    `json:\"has_more\"`\n\tCursor  string `json:\"cursor\"`\n}\n\nfunc (p Page) HasNextPage() bool {\n\treturn p.HasMore == 1\n}\n\ntype Root = model.Object\n\ntype (\n\tFileListResp struct {\n\t\tPage\n\t\tList []File `json:\"list\"`\n\t}\n\n\tFile struct {\n\t\tFsid     int64    `json:\"fsid\"` // 文件ID\n\t\tPath     string   `json:\"path\"` // 文件路径\n\t\tSize     int64    `json:\"size\"`\n\t\tCtime    int64    `json:\"ctime\"` // 创建时间 s\n\t\tMtime    int64    `json:\"mtime\"` // 修改时间 s\n\t\tThumburl []string `json:\"thumburl\"`\n\t\tMd5      string   `json:\"md5\"`\n\t}\n)\n\nfunc (c *File) GetSize() int64        { return c.Size }\nfunc (c *File) GetName() string       { return getFileName(c.Path) }\nfunc (c *File) CreateTime() time.Time { return time.Unix(c.Ctime, 0) }\nfunc (c *File) ModTime() time.Time    { return time.Unix(c.Mtime, 0) }\nfunc (c *File) IsDir() bool           { return false }\nfunc (c *File) GetID() string         { return \"\" }\nfunc (c *File) GetPath() string       { return \"\" }\nfunc (c *File) Thumb() string {\n\tif len(c.Thumburl) > 0 {\n\t\treturn c.Thumburl[0]\n\t}\n\treturn \"\"\n}\n\nfunc (c *File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.MD5, DecryptMd5(c.Md5))\n}\n\n/*相册部分*/\ntype (\n\tAlbumListResp struct {\n\t\tPage\n\t\tList       []Album `json:\"list\"`\n\t\tReset      int64   `json:\"reset\"`\n\t\tTotalCount int64   `json:\"total_count\"`\n\t}\n\n\tAlbum struct {\n\t\tAlbumID      string `json:\"album_id\"`\n\t\tTid          int64  `json:\"tid\"`\n\t\tTitle        string `json:\"title\"`\n\t\tJoinTime     int64  `json:\"join_time\"`\n\t\tCreationTime int64  `json:\"create_time\"`\n\t\tMtime        int64  `json:\"mtime\"`\n\n\t\tparseTime *time.Time\n\t}\n\n\tAlbumFileListResp struct {\n\t\tPage\n\t\tList       []AlbumFile `json:\"list\"`\n\t\tReset      int64       `json:\"reset\"`\n\t\tTotalCount int64       `json:\"total_count\"`\n\t}\n\n\tAlbumFile struct {\n\t\tFile\n\t\tAlbumID string `json:\"album_id\"`\n\t\tTid     int64  `json:\"tid\"`\n\t\tUk      int64  `json:\"uk\"`\n\t}\n)\n\nfunc (a *Album) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (a *Album) GetSize() int64        { return 0 }\nfunc (a *Album) GetName() string       { return a.Title }\nfunc (a *Album) CreateTime() time.Time { return time.Unix(a.CreationTime, 0) }\nfunc (a *Album) ModTime() time.Time    { return time.Unix(a.Mtime, 0) }\nfunc (a *Album) IsDir() bool           { return true }\nfunc (a *Album) GetID() string         { return \"\" }\nfunc (a *Album) GetPath() string       { return \"\" }\n\ntype (\n\tCopyFileResp struct {\n\t\tList []CopyFile `json:\"list\"`\n\t}\n\tCopyFile struct {\n\t\tFromFsid  int64  `json:\"from_fsid\"` // 源ID\n\t\tCtime     int64  `json:\"ctime\"`\n\t\tFsid      int64  `json:\"fsid\"` // 目标ID\n\t\tPath      string `json:\"path\"`\n\t\tShootTime int    `json:\"shoot_time\"`\n\t}\n)\n\n/*上传部分*/\ntype (\n\tUploadFile struct {\n\t\tFsID           int64  `json:\"fs_id\"`\n\t\tSize           int64  `json:\"size\"`\n\t\tMd5            string `json:\"md5\"`\n\t\tServerFilename string `json:\"server_filename\"`\n\t\tPath           string `json:\"path\"`\n\t\tCtime          int64  `json:\"ctime\"`\n\t\tMtime          int64  `json:\"mtime\"`\n\t\tIsdir          int    `json:\"isdir\"`\n\t\tCategory       int    `json:\"category\"`\n\t\tServerMd5      string `json:\"server_md5\"`\n\t\tShootTime      int    `json:\"shoot_time\"`\n\t}\n\n\tCreateFileResp struct {\n\t\tData UploadFile `json:\"data\"`\n\t}\n\n\tPrecreateResp struct {\n\t\tReturnType int `json:\"return_type\"` //存在返回2 不存在返回1 已经保存3\n\t\t//存在返回\n\t\tCreateFileResp\n\n\t\t//不存在返回\n\t\tPath      string `json:\"path\"`\n\t\tUploadID  string `json:\"uploadid\"`\n\t\tBlockList []int  `json:\"block_list\"`\n\t}\n)\n\nfunc (f *UploadFile) toFile() *File {\n\treturn &File{\n\t\tFsid:     f.FsID,\n\t\tPath:     f.Path,\n\t\tSize:     f.Size,\n\t\tCtime:    f.Ctime,\n\t\tMtime:    f.Mtime,\n\t\tThumburl: nil,\n\t}\n}\n\n/* 共享相册部分 */\ntype InviteResp struct {\n\tPdata struct {\n\t\t// 邀请码\n\t\tInviteCode string `json:\"invite_code\"`\n\t\t// 有效时间\n\t\tExpireTime int    `json:\"expire_time\"`\n\t\tShareID    string `json:\"share_id\"`\n\t} `json:\"pdata\"`\n}\n\n/* 加入相册部分 */\ntype JoinOrCreateAlbumResp struct {\n\tAlbumID       string `json:\"album_id\"`\n\tAlreadyExists int    `json:\"already_exists\"`\n}\n"
  },
  {
    "path": "drivers/baidu_photo/utils.go",
    "content": "package baiduphoto\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tAPI_URL         = \"https://photo.baidu.com/youai\"\n\tUSER_API_URL    = API_URL + \"/user/v1\"\n\tALBUM_API_URL   = API_URL + \"/album/v1\"\n\tFILE_API_URL_V1 = API_URL + \"/file/v1\"\n\tFILE_API_URL_V2 = API_URL + \"/file/v2\"\n)\n\nfunc (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {\n\treq := client.R().\n\t\t// SetQueryParam(\"access_token\", d.AccessToken)\n\t\tSetHeader(\"Cookie\", d.Cookie)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, furl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terron := utils.Json.Get(res.Body(), \"errno\").ToInt()\n\tswitch erron {\n\tcase 0:\n\t\tbreak\n\tcase 50805:\n\t\treturn nil, fmt.Errorf(\"you have joined album\")\n\tcase 50820:\n\t\treturn nil, fmt.Errorf(\"no shared albums found\")\n\tcase 50100:\n\t\treturn nil, fmt.Errorf(\"illegal title, only supports 50 characters\")\n\t// case -6:\n\t// \tif err = d.refreshToken(); err != nil {\n\t// \t\treturn nil, err\n\t// \t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"errno: %d, refer to https://photo.baidu.com/union/doc\", erron)\n\t}\n\treturn res, nil\n}\n\n//func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n//\tres, err := d.request(furl, method, callback, resp)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\treturn res.Body(), nil\n//}\n\n// func (d *BaiduPhoto) refreshToken() error {\n// \tu := \"https://openapi.baidu.com/oauth/2.0/token\"\n// \tvar resp base.TokenResp\n// \tvar e TokenErrResp\n// \t_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{\n// \t\t\"grant_type\":    \"refresh_token\",\n// \t\t\"refresh_token\": d.RefreshToken,\n// \t\t\"client_id\":     d.ClientID,\n// \t\t\"client_secret\": d.ClientSecret,\n// \t}).Get(u)\n// \tif err != nil {\n// \t\treturn err\n// \t}\n// \tif e.ErrorMsg != \"\" {\n// \t\treturn &e\n// \t}\n// \tif resp.RefreshToken == \"\" {\n// \t\treturn errs.EmptyToken\n// \t}\n// \td.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken\n// \top.MustSaveDriverStorage(d)\n// \treturn nil\n// }\n\nfunc (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {\n\treturn d.Request(base.RestyClient, furl, http.MethodGet, callback, resp)\n}\n\nfunc (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {\n\treturn d.Request(base.RestyClient, furl, http.MethodPost, callback, resp)\n}\n\n// 获取所有文件\nfunc (d *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {\n\tvar cursor string\n\tfor {\n\t\tvar resp FileListResp\n\t\t_, err = d.Get(FILE_API_URL_V1+\"/list\", func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"need_thumbnail\":     \"1\",\n\t\t\t\t\"need_filter_hidden\": \"0\",\n\t\t\t\t\"cursor\":             cursor,\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tfiles = append(files, resp.List...)\n\t\tif !resp.HasNextPage() {\n\t\t\treturn\n\t\t}\n\t\tcursor = resp.Cursor\n\t}\n}\n\n// 删除根文件\nfunc (d *BaiduPhoto) DeleteFile(ctx context.Context, file *File) error {\n\t_, err := d.Get(FILE_API_URL_V1+\"/delete\", func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"fsid_list\": fmt.Sprintf(\"[%d]\", file.Fsid),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\n// 获取所有相册\nfunc (d *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) {\n\tvar cursor string\n\tfor {\n\t\tvar resp AlbumListResp\n\t\t_, err = d.Get(ALBUM_API_URL+\"/list\", func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"need_amount\": \"1\",\n\t\t\t\t\"limit\":       \"100\",\n\t\t\t\t\"cursor\":      cursor,\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tif albums == nil {\n\t\t\talbums = make([]Album, 0, resp.TotalCount)\n\t\t}\n\n\t\tcursor = resp.Cursor\n\t\talbums = append(albums, resp.List...)\n\n\t\tif !resp.HasNextPage() {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// 获取相册中所有文件\nfunc (d *BaiduPhoto) GetAllAlbumFile(ctx context.Context, album *Album, passwd string) (files []AlbumFile, err error) {\n\tvar cursor string\n\tfor {\n\t\tvar resp AlbumFileListResp\n\t\t_, err = d.Get(ALBUM_API_URL+\"/listfile\", func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"album_id\":    album.AlbumID,\n\t\t\t\t\"need_amount\": \"1\",\n\t\t\t\t\"limit\":       \"1000\",\n\t\t\t\t\"passwd\":      passwd,\n\t\t\t\t\"cursor\":      cursor,\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tif files == nil {\n\t\t\tfiles = make([]AlbumFile, 0, resp.TotalCount)\n\t\t}\n\n\t\tcursor = resp.Cursor\n\t\tfiles = append(files, resp.List...)\n\n\t\tif !resp.HasNextPage() {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// 创建相册\nfunc (d *BaiduPhoto) CreateAlbum(ctx context.Context, name string) (*Album, error) {\n\tvar resp JoinOrCreateAlbumResp\n\t_, err := d.Post(ALBUM_API_URL+\"/create\", func(r *resty.Request) {\n\t\tr.SetContext(ctx).SetResult(&resp)\n\t\tr.SetQueryParams(map[string]string{\n\t\t\t\"title\":  name,\n\t\t\t\"tid\":    getTid(),\n\t\t\t\"source\": \"0\",\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.GetAlbumDetail(ctx, resp.AlbumID)\n}\n\n// 相册改名\nfunc (d *BaiduPhoto) SetAlbumName(ctx context.Context, album *Album, name string) (*Album, error) {\n\t_, err := d.Post(ALBUM_API_URL+\"/settitle\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetFormData(map[string]string{\n\t\t\t\"title\":    name,\n\t\t\t\"album_id\": album.AlbumID,\n\t\t\t\"tid\":      fmt.Sprint(album.Tid),\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn renameAlbum(album, name), nil\n}\n\n// 删除相册\nfunc (d *BaiduPhoto) DeleteAlbum(ctx context.Context, album *Album) error {\n\t_, err := d.Post(ALBUM_API_URL+\"/delete\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetFormData(map[string]string{\n\t\t\t\"album_id\":            album.AlbumID,\n\t\t\t\"tid\":                 fmt.Sprint(album.Tid),\n\t\t\t\"delete_origin_image\": BoolToIntStr(d.DeleteOrigin), // 是否删除原图 0 不删除 1 删除\n\t\t})\n\t}, nil)\n\treturn err\n}\n\n// 删除相册文件\nfunc (d *BaiduPhoto) DeleteAlbumFile(ctx context.Context, file *AlbumFile) error {\n\t_, err := d.Post(ALBUM_API_URL+\"/delfile\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetFormData(map[string]string{\n\t\t\t\"album_id\":   fmt.Sprint(file.AlbumID),\n\t\t\t\"tid\":        fmt.Sprint(file.Tid),\n\t\t\t\"list\":       fmt.Sprintf(`[{\"fsid\":%d,\"uk\":%d}]`, file.Fsid, file.Uk),\n\t\t\t\"del_origin\": BoolToIntStr(d.DeleteOrigin), // 是否删除原图 0 不删除 1 删除\n\t\t})\n\t}, nil)\n\treturn err\n}\n\n// 增加相册文件\nfunc (d *BaiduPhoto) AddAlbumFile(ctx context.Context, album *Album, file *File) (*AlbumFile, error) {\n\t_, err := d.Get(ALBUM_API_URL+\"/addfile\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetQueryParams(map[string]string{\n\t\t\t\"album_id\": fmt.Sprint(album.AlbumID),\n\t\t\t\"tid\":      fmt.Sprint(album.Tid),\n\t\t\t\"list\":     fsidsFormatNotUk(file.Fsid),\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn moveFileToAlbumFile(file, album, d.Uk), nil\n}\n\n// 保存相册文件为根文件\nfunc (d *BaiduPhoto) CopyAlbumFile(ctx context.Context, file *AlbumFile) (*File, error) {\n\tvar resp CopyFileResp\n\t_, err := d.Post(ALBUM_API_URL+\"/copyfile\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetFormData(map[string]string{\n\t\t\t\"album_id\": file.AlbumID,\n\t\t\t\"tid\":      fmt.Sprint(file.Tid),\n\t\t\t\"uk\":       fmt.Sprint(file.Uk),\n\t\t\t\"list\":     fsidsFormatNotUk(file.Fsid),\n\t\t})\n\t\tr.SetResult(&resp)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn copyFile(file, &resp.List[0]), nil\n}\n\n// 加入相册\nfunc (d *BaiduPhoto) JoinAlbum(ctx context.Context, code string) (*Album, error) {\n\tvar resp InviteResp\n\t_, err := d.Get(ALBUM_API_URL+\"/querypcode\", func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"pcode\": code,\n\t\t\t\"web\":   \"1\",\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar resp2 JoinOrCreateAlbumResp\n\t_, err = d.Get(ALBUM_API_URL+\"/join\", func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"invite_code\": resp.Pdata.InviteCode,\n\t\t})\n\t}, &resp2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.GetAlbumDetail(ctx, resp2.AlbumID)\n}\n\n// 获取相册详细信息\nfunc (d *BaiduPhoto) GetAlbumDetail(ctx context.Context, albumID string) (*Album, error) {\n\tvar album Album\n\t_, err := d.Get(ALBUM_API_URL+\"/detail\", func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetResult(&album)\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"album_id\": albumID,\n\t\t})\n\t}, &album)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &album, nil\n}\n\nfunc (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.LinkArgs) (*model.Link, error) {\n\theaders := map[string]string{\n\t\t\"User-Agent\": base.UserAgent,\n\t}\n\tif args.Header.Get(\"User-Agent\") != \"\" {\n\t\theaders[\"User-Agent\"] = args.Header.Get(\"User-Agent\")\n\t}\n\tif !utils.IsLocalIPAddr(args.IP) {\n\t\theaders[\"X-Forwarded-For\"] = args.IP\n\t}\n\n\tresp, err := d.Request(base.NoRedirectClient, ALBUM_API_URL+\"/download\", http.MethodHead, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetHeaders(headers)\n\t\tr.SetQueryParams(map[string]string{\n\t\t\t\"fsid\":     fmt.Sprint(file.Fsid),\n\t\t\t\"album_id\": file.AlbumID,\n\t\t\t\"tid\":      fmt.Sprint(file.Tid),\n\t\t\t\"uk\":       fmt.Sprint(file.Uk),\n\t\t})\n\t}, nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode() != 302 {\n\t\treturn nil, fmt.Errorf(\"not found 302 redirect\")\n\t}\n\n\tlocation := resp.Header().Get(\"Location\")\n\n\tlink := &model.Link{\n\t\tURL: location,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{headers[\"User-Agent\"]},\n\t\t\t\"Referer\":    []string{\"https://photo.baidu.com/\"},\n\t\t},\n\t}\n\treturn link, nil\n}\n\nfunc (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkArgs) (*model.Link, error) {\n\theaders := map[string]string{\n\t\t\"User-Agent\": base.UserAgent,\n\t}\n\tif args.Header.Get(\"User-Agent\") != \"\" {\n\t\theaders[\"User-Agent\"] = args.Header.Get(\"User-Agent\")\n\t}\n\tif !utils.IsLocalIPAddr(args.IP) {\n\t\theaders[\"X-Forwarded-For\"] = args.IP\n\t}\n\n\tvar downloadUrl struct {\n\t\tDlink string `json:\"dlink\"`\n\t}\n\t_, err := d.Get(FILE_API_URL_V2+\"/download\", func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetHeaders(headers)\n\t\tr.SetQueryParams(map[string]string{\n\t\t\t\"fsid\": fmt.Sprint(file.Fsid),\n\t\t})\n\t}, &downloadUrl)\n\n\t// resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+\"/download\", http.MethodHead, func(r *resty.Request) {\n\t// \tr.SetContext(ctx)\n\t// \tr.SetHeaders(headers)\n\t// \tr.SetQueryParams(map[string]string{\n\t// \t\t\"fsid\": fmt.Sprint(file.Fsid),\n\t// \t})\n\t// }, nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// if resp.StatusCode() != 302 {\n\t// \treturn nil, fmt.Errorf(\"not found 302 redirect\")\n\t// }\n\n\t// location := resp.Header().Get(\"Location\")\n\tlink := &model.Link{\n\t\tURL: downloadUrl.Dlink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{headers[\"User-Agent\"]},\n\t\t\t\"Referer\":    []string{\"https://photo.baidu.com/\"},\n\t\t},\n\t}\n\treturn link, nil\n}\n\n/*func (d *BaiduPhoto) linkStreamAlbum(ctx context.Context, file *AlbumFile) (*model.Link, error) {\n\treturn &model.Link{\n\t\tHeader: http.Header{},\n\t\tWriter: func(w io.Writer) error {\n\t\t\tres, err := d.Get(ALBUM_API_URL+\"/streaming\", func(r *resty.Request) {\n\t\t\t\tr.SetContext(ctx)\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"fsid\":     fmt.Sprint(file.Fsid),\n\t\t\t\t\t\"album_id\": file.AlbumID,\n\t\t\t\t\t\"tid\":      fmt.Sprint(file.Tid),\n\t\t\t\t\t\"uk\":       fmt.Sprint(file.Uk),\n\t\t\t\t}).SetDoNotParseResponse(true)\n\t\t\t}, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer res.RawBody().Close()\n\t\t\t_, err = io.Copy(w, res.RawBody())\n\t\t\treturn err\n\t\t},\n\t}, nil\n}*/\n\n/*func (d *BaiduPhoto) linkStream(ctx context.Context, file *File) (*model.Link, error) {\n\treturn &model.Link{\n\t\tHeader: http.Header{},\n\t\tWriter: func(w io.Writer) error {\n\t\t\tres, err := d.Get(FILE_API_URL_V1+\"/streaming\", func(r *resty.Request) {\n\t\t\t\tr.SetContext(ctx)\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"fsid\": fmt.Sprint(file.Fsid),\n\t\t\t\t}).SetDoNotParseResponse(true)\n\t\t\t}, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer res.RawBody().Close()\n\t\t\t_, err = io.Copy(w, res.RawBody())\n\t\t\treturn err\n\t\t},\n\t}, nil\n}*/\n\n// 获取uk\nfunc (d *BaiduPhoto) uInfo() (*UInfo, error) {\n\tvar info UInfo\n\t_, err := d.Get(USER_API_URL+\"/getuinfo\", func(req *resty.Request) {\n\n\t}, &info)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &info, nil\n}\n\nfunc (d *BaiduPhoto) getBDStoken() (string, error) {\n\tvar info struct {\n\t\tResult struct {\n\t\t\tBdstoken string `json:\"bdstoken\"`\n\t\t\tToken    string `json:\"token\"`\n\t\t\tUk       int64  `json:\"uk\"`\n\t\t} `json:\"result\"`\n\t}\n\t_, err := d.Get(\"https://pan.baidu.com/api/gettemplatevariable?fields=[%22bdstoken%22,%22token%22,%22uk%22]\", nil, &info)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn info.Result.Bdstoken, nil\n}\n\nfunc DecryptMd5(encryptMd5 string) string {\n\tif _, err := hex.DecodeString(encryptMd5); err == nil {\n\t\treturn encryptMd5\n\t}\n\n\tvar out strings.Builder\n\tout.Grow(len(encryptMd5))\n\tfor i, n := 0, int64(0); i < len(encryptMd5); i++ {\n\t\tif i == 9 {\n\t\t\tn = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')\n\t\t} else {\n\t\t\tn, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)\n\t\t}\n\t\tout.WriteString(strconv.FormatInt(n^int64(15&i), 16))\n\t}\n\n\tencryptMd5 = out.String()\n\treturn encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]\n}\n\nfunc EncryptMd5(originalMd5 string) string {\n\treversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]\n\n\tvar out strings.Builder\n\tout.Grow(len(reversed))\n\tfor i, n := 0, int64(0); i < len(reversed); i++ {\n\t\tn, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)\n\t\tn ^= int64(15 & i)\n\t\tif i == 9 {\n\t\t\tout.WriteRune(rune(n) + 'g')\n\t\t} else {\n\t\t\tout.WriteString(strconv.FormatInt(n, 16))\n\t\t}\n\t}\n\treturn out.String()\n}\n"
  },
  {
    "path": "drivers/base/client.go",
    "content": "package base\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nvar (\n\tNoRedirectClient *resty.Client\n\tRestyClient      *resty.Client\n\tHttpClient       *http.Client\n)\n\nvar DefaultTimeout = time.Second * 30\n\nconst UserAgent = \"Mozilla/5.0 (Macintosh; Apple macOS 26_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/425.6.30\"\nconst UserAgentNT = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/425.6.30\"\n\nfunc InitClient() {\n\tNoRedirectClient = resty.New().SetRedirectPolicy(\n\t\tresty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t}),\n\t).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})\n\tNoRedirectClient.SetHeader(\"user-agent\", UserAgent)\n\tnet.SetRestyProxyIfConfigured(NoRedirectClient)\n\n\tRestyClient = NewRestyClient()\n\tHttpClient = net.NewHttpClient()\n}\n\nfunc NewRestyClient() *resty.Client {\n\tclient := resty.New().\n\t\tSetHeader(\"user-agent\", UserAgent).\n\t\tSetRetryCount(3).\n\t\tSetRetryResetReaders(true).\n\t\tSetTimeout(DefaultTimeout).\n\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})\n\n\tnet.SetRestyProxyIfConfigured(client)\n\treturn client\n}\n"
  },
  {
    "path": "drivers/base/types.go",
    "content": "package base\n\nimport \"github.com/go-resty/resty/v2\"\n\ntype Json map[string]interface{}\n\ntype TokenResp struct {\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n}\n\ntype ReqCallback func(req *resty.Request)\n"
  },
  {
    "path": "drivers/base/upload.go",
    "content": "package base\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/go-cache\"\n)\n\n// storage upload progress, for upload recovery\nvar UploadStateCache = cache.NewMemCache(cache.WithShards[any](32))\n\n// Save upload progress for 20 minutes\nfunc SaveUploadProgress(driver driver.Driver, state any, keys ...string) bool {\n\treturn UploadStateCache.Set(\n\t\tfmt.Sprint(driver.Config().Name, \"-upload-\", strings.Join(keys, \"-\")),\n\t\tstate,\n\t\tcache.WithEx[any](time.Minute*20))\n}\n\n// An upload progress can only be made by one process alone,\n// so here you need to get it and then delete it.\nfunc GetUploadProgress[T any](driver driver.Driver, keys ...string) (state T, ok bool) {\n\tv, ok := UploadStateCache.GetDel(fmt.Sprint(driver.Config().Name, \"-upload-\", strings.Join(keys, \"-\")))\n\tif ok {\n\t\tstate, ok = v.(T)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "drivers/base/util.go",
    "content": "package base\n"
  },
  {
    "path": "drivers/chaoxing/driver.go",
    "content": "package chaoxing\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"google.golang.org/appengine/log\"\n)\n\ntype ChaoXing struct {\n\tmodel.Storage\n\tAddition\n\tcron   *cron.Cron\n\tconfig driver.Config\n\tconf   Conf\n}\n\nfunc (d *ChaoXing) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *ChaoXing) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *ChaoXing) refreshCookie() error {\n\tcookie, err := d.Login()\n\tif err != nil {\n\t\td.Status = err.Error()\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\td.Addition.Cookie = cookie\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *ChaoXing) Init(ctx context.Context) error {\n\terr := d.refreshCookie()\n\tif err != nil {\n\t\tlog.Errorf(ctx, err.Error())\n\t}\n\td.cron = cron.NewCron(time.Hour * 12)\n\td.cron.Do(func() {\n\t\terr = d.refreshCookie()\n\t\tif err != nil {\n\t\t\tlog.Errorf(ctx, err.Error())\n\t\t}\n\t})\n\treturn nil\n}\n\nfunc (d *ChaoXing) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\treturn nil\n}\n\nfunc (d *ChaoXing) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.GetFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *ChaoXing) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp DownResp\n\tua := d.conf.ua\n\tfileId := strings.Split(file.GetID(), \"$\")[1]\n\t_, err := d.requestDownload(\"/screen/note_note/files/status/\"+fileId, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"User-Agent\", ua)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu := resp.Download\n\treturn &model.Link{\n\t\tURL: u,\n\t\tHeader: http.Header{\n\t\t\t\"Cookie\":     []string{d.Cookie},\n\t\t\t\"Referer\":    []string{d.conf.referer},\n\t\t\t\"User-Agent\": []string{ua},\n\t\t},\n\t\tConcurrency: 2,\n\t\tPartSize:    10 * utils.MB,\n\t}, nil\n}\n\nfunc (d *ChaoXing) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tquery := map[string]string{\n\t\t\"bbsid\": d.Addition.Bbsid,\n\t\t\"name\":  dirName,\n\t\t\"pid\":   parentDir.GetID(),\n\t}\n\tvar resp ListFileResp\n\t_, err := d.request(\"/pc/resource/addResourceFolder\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Result != 1 {\n\t\tmsg := fmt.Sprintf(\"error:%s\", resp.Msg)\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\nfunc (d *ChaoXing) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tquery := map[string]string{\n\t\t\"bbsid\":     d.Addition.Bbsid,\n\t\t\"folderIds\": srcObj.GetID(),\n\t\t\"targetId\":  dstDir.GetID(),\n\t}\n\tif !srcObj.IsDir() {\n\t\tquery = map[string]string{\n\t\t\t\"bbsid\":    d.Addition.Bbsid,\n\t\t\t\"recIds\":   strings.Split(srcObj.GetID(), \"$\")[0],\n\t\t\t\"targetId\": dstDir.GetID(),\n\t\t}\n\t}\n\tvar resp ListFileResp\n\t_, err := d.request(\"/pc/resource/moveResource\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !resp.Status {\n\t\tmsg := fmt.Sprintf(\"error:%s\", resp.Msg)\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\nfunc (d *ChaoXing) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tquery := map[string]string{\n\t\t\"bbsid\":    d.Addition.Bbsid,\n\t\t\"folderId\": srcObj.GetID(),\n\t\t\"name\":     newName,\n\t}\n\tpath := \"/pc/resource/updateResourceFolderName\"\n\tif !srcObj.IsDir() {\n\t\t// path = \"/pc/resource/updateResourceFileName\"\n\t\t// query = map[string]string{\n\t\t// \t\"bbsid\":    d.Addition.Bbsid,\n\t\t// \t\"recIds\":   strings.Split(srcObj.GetID(), \"$\")[0],\n\t\t// \t\"name\":     newName,\n\t\t// }\n\t\treturn errors.New(\"此网盘不支持修改文件名\")\n\t}\n\tvar resp ListFileResp\n\t_, err := d.request(path, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Result != 1 {\n\t\tmsg := fmt.Sprintf(\"error:%s\", resp.Msg)\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\nfunc (d *ChaoXing) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO copy obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *ChaoXing) Remove(ctx context.Context, obj model.Obj) error {\n\tquery := map[string]string{\n\t\t\"bbsid\":     d.Addition.Bbsid,\n\t\t\"folderIds\": obj.GetID(),\n\t}\n\tpath := \"/pc/resource/deleteResourceFolder\"\n\tvar resp ListFileResp\n\tif !obj.IsDir() {\n\t\tpath = \"/pc/resource/deleteResourceFile\"\n\t\tquery = map[string]string{\n\t\t\t\"bbsid\":  d.Addition.Bbsid,\n\t\t\t\"recIds\": strings.Split(obj.GetID(), \"$\")[0],\n\t\t}\n\t}\n\t_, err := d.request(path, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Result != 1 {\n\t\tmsg := fmt.Sprintf(\"error:%s\", resp.Msg)\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\nfunc (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tvar resp UploadDataRsp\n\t_, err := d.request(\"https://noteyd.chaoxing.com/pc/files/getUploadConfig\", http.MethodGet, func(req *resty.Request) {\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Result != 1 {\n\t\treturn errors.New(\"get upload data error\")\n\t}\n\tbody := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))\n\twriter := multipart.NewWriter(body)\n\t_, err = writer.CreateFormFile(\"file\", file.GetName())\n\tif err != nil {\n\t\treturn err\n\t}\n\theadSize := body.Len()\n\terr = writer.WriteField(\"_token\", resp.Msg.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = writer.WriteField(\"puid\", strconv.Itoa(resp.Msg.Puid))\n\tif err != nil {\n\t\tfmt.Println(\"Error writing param2 to request body:\", err)\n\t\treturn err\n\t}\n\terr = writer.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\thead := bytes.NewReader(body.Bytes()[:headSize])\n\ttail := bytes.NewReader(body.Bytes()[headSize:])\n\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\tReader: io.MultiReader(head, file, tail),\n\t\t\tSize:   int64(body.Len()) + file.GetSize(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, \"https://pan-yz.chaoxing.com/upload\", r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\treq.ContentLength = int64(body.Len()) + file.GetSize()\n\tresps, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resps.Body.Close()\n\tbody.Reset()\n\t_, err = body.ReadFrom(resps.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar fileRsp UploadFileDataRsp\n\terr = json.Unmarshal(body.Bytes(), &fileRsp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fileRsp.Msg != \"success\" {\n\t\treturn errors.New(fileRsp.Msg)\n\t}\n\tuploadDoneParam := UploadDoneParam{Key: fileRsp.ObjectID, Cataid: \"100000019\", Param: fileRsp.Data}\n\tparams, err := json.Marshal(uploadDoneParam)\n\tif err != nil {\n\t\treturn err\n\t}\n\tquery := map[string]string{\n\t\t\"bbsid\":  d.Addition.Bbsid,\n\t\t\"pid\":    dstDir.GetID(),\n\t\t\"type\":   \"yunpan\",\n\t\t\"params\": url.QueryEscape(\"[\" + string(params) + \"]\"),\n\t}\n\tvar respd ListFileResp\n\t_, err = d.request(\"/pc/resource/addResource\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &respd)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif respd.Result != 1 {\n\t\tmsg := fmt.Sprintf(\"error:%v\", resp.Msg)\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\nvar _ driver.Driver = (*ChaoXing)(nil)\n"
  },
  {
    "path": "drivers/chaoxing/meta.go",
    "content": "package chaoxing\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\n// 此程序挂载的是超星小组网盘，需要代理才能使用；\n// 登录超星后进入个人空间，进入小组，新建小组，点击进去。\n// url中就有bbsid的参数，系统限制单文件大小2G，没有总容量限制\ntype Addition struct {\n\t// 超星用户名及密码\n\tUserName string `json:\"user_name\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\t// 从自己新建的小组url里获取\n\tBbsid string `json:\"bbsid\" required:\"true\"`\n\tdriver.RootID\n\t// 可不填，程序会自动登录获取\n\tCookie string `json:\"cookie\"`\n}\n\ntype Conf struct {\n\tua         string\n\treferer    string\n\tapi        string\n\tDowloadApi string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ChaoXing{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"ChaoXingGroupDrive\",\n\t\t\t\tOnlyProxy:         true,\n\t\t\t\tDefaultRoot:       \"-1\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tua:         \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch\",\n\t\t\t\treferer:    \"https://chaoxing.com/\",\n\t\t\t\tapi:        \"https://groupweb.chaoxing.com\",\n\t\t\t\tDowloadApi: \"https://noteyd.chaoxing.com\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/chaoxing/types.go",
    "content": "package chaoxing\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Resp struct {\n\tResult int `json:\"result\"`\n}\n\ntype UserAuth struct {\n\tGroupAuth struct {\n\t\tAddData                 int    `json:\"addData\"`\n\t\tAddDataFolder           int    `json:\"addDataFolder\"`\n\t\tAddLebel                int    `json:\"addLebel\"`\n\t\tAddManager              int    `json:\"addManager\"`\n\t\tAddMem                  int    `json:\"addMem\"`\n\t\tAddTopicFolder          int    `json:\"addTopicFolder\"`\n\t\tAnonymousAddReply       int    `json:\"anonymousAddReply\"`\n\t\tAnonymousAddTopic       int    `json:\"anonymousAddTopic\"`\n\t\tBatchOperation          int    `json:\"batchOperation\"`\n\t\tDelData                 int    `json:\"delData\"`\n\t\tDelDataFolder           int    `json:\"delDataFolder\"`\n\t\tDelMem                  int    `json:\"delMem\"`\n\t\tDelTopicFolder          int    `json:\"delTopicFolder\"`\n\t\tDismiss                 int    `json:\"dismiss\"`\n\t\tExamEnc                 string `json:\"examEnc\"`\n\t\tGroupChat               int    `json:\"groupChat\"`\n\t\tIsShowCircleChatButton  int    `json:\"isShowCircleChatButton\"`\n\t\tIsShowCircleCloudButton int    `json:\"isShowCircleCloudButton\"`\n\t\tIsShowCompanyButton     int    `json:\"isShowCompanyButton\"`\n\t\tJoin                    int    `json:\"join\"`\n\t\tMemberShowRankSet       int    `json:\"memberShowRankSet\"`\n\t\tModifyDataFolder        int    `json:\"modifyDataFolder\"`\n\t\tModifyExpose            int    `json:\"modifyExpose\"`\n\t\tModifyName              int    `json:\"modifyName\"`\n\t\tModifyShowPic           int    `json:\"modifyShowPic\"`\n\t\tModifyTopicFolder       int    `json:\"modifyTopicFolder\"`\n\t\tModifyVisibleState      int    `json:\"modifyVisibleState\"`\n\t\tOnlyMgrScoreSet         int    `json:\"onlyMgrScoreSet\"`\n\t\tQuit                    int    `json:\"quit\"`\n\t\tSendNotice              int    `json:\"sendNotice\"`\n\t\tShowActivityManage      int    `json:\"showActivityManage\"`\n\t\tShowActivitySet         int    `json:\"showActivitySet\"`\n\t\tShowAttentionSet        int    `json:\"showAttentionSet\"`\n\t\tShowAutoClearStatus     int    `json:\"showAutoClearStatus\"`\n\t\tShowBarcode             int    `json:\"showBarcode\"`\n\t\tShowChatRoomSet         int    `json:\"showChatRoomSet\"`\n\t\tShowCircleActivitySet   int    `json:\"showCircleActivitySet\"`\n\t\tShowCircleSet           int    `json:\"showCircleSet\"`\n\t\tShowCmem                int    `json:\"showCmem\"`\n\t\tShowDataFolder          int    `json:\"showDataFolder\"`\n\t\tShowDelReason           int    `json:\"showDelReason\"`\n\t\tShowForward             int    `json:\"showForward\"`\n\t\tShowGroupChat           int    `json:\"showGroupChat\"`\n\t\tShowGroupChatSet        int    `json:\"showGroupChatSet\"`\n\t\tShowGroupSquareSet      int    `json:\"showGroupSquareSet\"`\n\t\tShowLockAddSet          int    `json:\"showLockAddSet\"`\n\t\tShowManager             int    `json:\"showManager\"`\n\t\tShowManagerIdentitySet  int    `json:\"showManagerIdentitySet\"`\n\t\tShowNeedDelReasonSet    int    `json:\"showNeedDelReasonSet\"`\n\t\tShowNotice              int    `json:\"showNotice\"`\n\t\tShowOnlyManagerReplySet int    `json:\"showOnlyManagerReplySet\"`\n\t\tShowRank                int    `json:\"showRank\"`\n\t\tShowRank2               int    `json:\"showRank2\"`\n\t\tShowRecycleBin          int    `json:\"showRecycleBin\"`\n\t\tShowReplyByClass        int    `json:\"showReplyByClass\"`\n\t\tShowReplyNeedCheck      int    `json:\"showReplyNeedCheck\"`\n\t\tShowSignbanSet          int    `json:\"showSignbanSet\"`\n\t\tShowSpeechSet           int    `json:\"showSpeechSet\"`\n\t\tShowTopicCheck          int    `json:\"showTopicCheck\"`\n\t\tShowTopicNeedCheck      int    `json:\"showTopicNeedCheck\"`\n\t\tShowTransferSet         int    `json:\"showTransferSet\"`\n\t} `json:\"groupAuth\"`\n\tOperationAuth struct {\n\t\tAdd                int `json:\"add\"`\n\t\tAddTopicToFolder   int `json:\"addTopicToFolder\"`\n\t\tChoiceSet          int `json:\"choiceSet\"`\n\t\tDelTopicFromFolder int `json:\"delTopicFromFolder\"`\n\t\tDelete             int `json:\"delete\"`\n\t\tReply              int `json:\"reply\"`\n\t\tScoreSet           int `json:\"scoreSet\"`\n\t\tTopSet             int `json:\"topSet\"`\n\t\tUpdate             int `json:\"update\"`\n\t} `json:\"operationAuth\"`\n}\n\n// 手机端学习通上传的文件的json内容(content字段)与网页端上传的有所不同\n// 网页端json `\"puid\": 54321, \"size\": 12345`\n// 手机端json `\"puid\": \"54321\". \"size\": \"12345\"`\ntype int_str int\n\n// json 字符串数字和纯数字解析\nfunc (ios *int_str) UnmarshalJSON(data []byte) error {\n\tintValue, err := strconv.Atoi(string(bytes.Trim(data, \"\\\"\")))\n\tif err != nil {\n\t\treturn err\n\t}\n\t*ios = int_str(intValue)\n\treturn nil\n}\n\ntype File struct {\n\tCataid  int `json:\"cataid\"`\n\tCfid    int `json:\"cfid\"`\n\tContent struct {\n\t\tCfid             int     `json:\"cfid\"`\n\t\tPid              int     `json:\"pid\"`\n\t\tFolderName       string  `json:\"folderName\"`\n\t\tShareType        int     `json:\"shareType\"`\n\t\tPreview          string  `json:\"preview\"`\n\t\tFiletype         string  `json:\"filetype\"`\n\t\tPreviewURL       string  `json:\"previewUrl\"`\n\t\tIsImg            bool    `json:\"isImg\"`\n\t\tParentPath       string  `json:\"parentPath\"`\n\t\tIcon             string  `json:\"icon\"`\n\t\tSuffix           string  `json:\"suffix\"`\n\t\tDuration         int     `json:\"duration\"`\n\t\tPantype          string  `json:\"pantype\"`\n\t\tPuid             int_str `json:\"puid\"`\n\t\tFilepath         string  `json:\"filepath\"`\n\t\tCrc              string  `json:\"crc\"`\n\t\tIsfile           bool    `json:\"isfile\"`\n\t\tResidstr         string  `json:\"residstr\"`\n\t\tObjectID         string  `json:\"objectId\"`\n\t\tExtinfo          string  `json:\"extinfo\"`\n\t\tThumbnail        string  `json:\"thumbnail\"`\n\t\tCreator          int     `json:\"creator\"`\n\t\tResTypeValue     int     `json:\"resTypeValue\"`\n\t\tUploadDateFormat string  `json:\"uploadDateFormat\"`\n\t\tDisableOpt       bool    `json:\"disableOpt\"`\n\t\tDownPath         string  `json:\"downPath\"`\n\t\tSort             int     `json:\"sort\"`\n\t\tTopsort          int     `json:\"topsort\"`\n\t\tRestype          string  `json:\"restype\"`\n\t\tSize             int_str `json:\"size\"`\n\t\tUploadDate       int64   `json:\"uploadDate\"`\n\t\tFileSize         string  `json:\"fileSize\"`\n\t\tName             string  `json:\"name\"`\n\t\tFileID           string  `json:\"fileId\"`\n\t} `json:\"content\"`\n\tCreatorID  int    `json:\"creatorId\"`\n\tDesID      string `json:\"des_id\"`\n\tID         int    `json:\"id\"`\n\tInserttime int64  `json:\"inserttime\"`\n\tKey        string `json:\"key\"`\n\tNorder     int    `json:\"norder\"`\n\tOwnerID    int    `json:\"ownerId\"`\n\tOwnerType  int    `json:\"ownerType\"`\n\tPath       string `json:\"path\"`\n\tRid        int    `json:\"rid\"`\n\tStatus     int    `json:\"status\"`\n\tTopsign    int    `json:\"topsign\"`\n}\n\ntype ListFileResp struct {\n\tMsg      string   `json:\"msg\"`\n\tResult   int      `json:\"result\"`\n\tStatus   bool     `json:\"status\"`\n\tUserAuth UserAuth `json:\"userAuth\"`\n\tList     []File   `json:\"list\"`\n}\n\ntype DownResp struct {\n\tMsg        string `json:\"msg\"`\n\tDuration   int    `json:\"duration\"`\n\tDownload   string `json:\"download\"`\n\tFileStatus string `json:\"fileStatus\"`\n\tURL        string `json:\"url\"`\n\tStatus     bool   `json:\"status\"`\n}\n\ntype UploadDataRsp struct {\n\tResult int `json:\"result\"`\n\tMsg    struct {\n\t\tPuid  int    `json:\"puid\"`\n\t\tToken string `json:\"token\"`\n\t} `json:\"msg\"`\n}\n\ntype UploadFileDataRsp struct {\n\tResult   bool   `json:\"result\"`\n\tMsg      string `json:\"msg\"`\n\tCrc      string `json:\"crc\"`\n\tObjectID string `json:\"objectId\"`\n\tResid    int64  `json:\"resid\"`\n\tPuid     int    `json:\"puid\"`\n\tData     struct {\n\t\tDisableOpt       bool   `json:\"disableOpt\"`\n\t\tResid            int64  `json:\"resid\"`\n\t\tCrc              string `json:\"crc\"`\n\t\tPuid             int    `json:\"puid\"`\n\t\tIsfile           bool   `json:\"isfile\"`\n\t\tPantype          string `json:\"pantype\"`\n\t\tSize             int    `json:\"size\"`\n\t\tName             string `json:\"name\"`\n\t\tObjectID         string `json:\"objectId\"`\n\t\tRestype          string `json:\"restype\"`\n\t\tUploadDate       int64  `json:\"uploadDate\"`\n\t\tModifyDate       int64  `json:\"modifyDate\"`\n\t\tUploadDateFormat string `json:\"uploadDateFormat\"`\n\t\tResidstr         string `json:\"residstr\"`\n\t\tSuffix           string `json:\"suffix\"`\n\t\tPreview          string `json:\"preview\"`\n\t\tThumbnail        string `json:\"thumbnail\"`\n\t\tCreator          int    `json:\"creator\"`\n\t\tDuration         int    `json:\"duration\"`\n\t\tIsImg            bool   `json:\"isImg\"`\n\t\tPreviewURL       string `json:\"previewUrl\"`\n\t\tFiletype         string `json:\"filetype\"`\n\t\tFilepath         string `json:\"filepath\"`\n\t\tSort             int    `json:\"sort\"`\n\t\tTopsort          int    `json:\"topsort\"`\n\t\tResTypeValue     int    `json:\"resTypeValue\"`\n\t\tExtinfo          string `json:\"extinfo\"`\n\t} `json:\"data\"`\n}\n\ntype UploadDoneParam struct {\n\tCataid string `json:\"cataid\"`\n\tKey    string `json:\"key\"`\n\tParam  struct {\n\t\tDisableOpt       bool   `json:\"disableOpt\"`\n\t\tResid            int64  `json:\"resid\"`\n\t\tCrc              string `json:\"crc\"`\n\t\tPuid             int    `json:\"puid\"`\n\t\tIsfile           bool   `json:\"isfile\"`\n\t\tPantype          string `json:\"pantype\"`\n\t\tSize             int    `json:\"size\"`\n\t\tName             string `json:\"name\"`\n\t\tObjectID         string `json:\"objectId\"`\n\t\tRestype          string `json:\"restype\"`\n\t\tUploadDate       int64  `json:\"uploadDate\"`\n\t\tModifyDate       int64  `json:\"modifyDate\"`\n\t\tUploadDateFormat string `json:\"uploadDateFormat\"`\n\t\tResidstr         string `json:\"residstr\"`\n\t\tSuffix           string `json:\"suffix\"`\n\t\tPreview          string `json:\"preview\"`\n\t\tThumbnail        string `json:\"thumbnail\"`\n\t\tCreator          int    `json:\"creator\"`\n\t\tDuration         int    `json:\"duration\"`\n\t\tIsImg            bool   `json:\"isImg\"`\n\t\tPreviewURL       string `json:\"previewUrl\"`\n\t\tFiletype         string `json:\"filetype\"`\n\t\tFilepath         string `json:\"filepath\"`\n\t\tSort             int    `json:\"sort\"`\n\t\tTopsort          int    `json:\"topsort\"`\n\t\tResTypeValue     int    `json:\"resTypeValue\"`\n\t\tExtinfo          string `json:\"extinfo\"`\n\t} `json:\"param\"`\n}\n\nfunc fileToObj(f File) *model.Object {\n\tif len(f.Content.FolderName) > 0 {\n\t\treturn &model.Object{\n\t\t\tID:       strconv.Itoa(f.ID),\n\t\t\tName:     f.Content.FolderName,\n\t\t\tSize:     0,\n\t\t\tModified: time.UnixMilli(f.Inserttime),\n\t\t\tIsFolder: true,\n\t\t}\n\t}\n\tpaserTime := time.UnixMilli(f.Content.UploadDate)\n\treturn &model.Object{\n\t\tID:       fmt.Sprintf(\"%d$%s\", f.ID, f.Content.FileID),\n\t\tName:     f.Content.Name,\n\t\tSize:     int64(f.Content.Size),\n\t\tModified: paserTime,\n\t\tIsFolder: false,\n\t}\n}\n"
  },
  {
    "path": "drivers/chaoxing/util.go",
    "content": "package chaoxing\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *ChaoXing) requestDownload(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tu := d.conf.DowloadApi + pathname\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":  d.Cookie,\n\t\t\"Accept\":  \"application/json, text/plain, */*\",\n\t\t\"Referer\": d.conf.referer,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Resp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *ChaoXing) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tu := d.conf.api + pathname\n\tif strings.Contains(pathname, \"getUploadConfig\") {\n\t\tu = pathname\n\t}\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":  d.Cookie,\n\t\t\"Accept\":  \"application/json, text/plain, */*\",\n\t\t\"Referer\": d.conf.referer,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Resp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *ChaoXing) GetFiles(parent string) ([]File, error) {\n\tfiles := make([]File, 0)\n\tquery := map[string]string{\n\t\t\"bbsid\":    d.Addition.Bbsid,\n\t\t\"folderId\": parent,\n\t\t\"recType\":  \"1\",\n\t}\n\tvar resp ListFileResp\n\t_, err := d.request(\"/pc/resource/getResourceList\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Result != 1 {\n\t\tmsg := fmt.Sprintf(\"error code is:%d\", resp.Result)\n\t\treturn nil, errors.New(msg)\n\t}\n\tif len(resp.List) > 0 {\n\t\tfiles = append(files, resp.List...)\n\t}\n\tquerys := map[string]string{\n\t\t\"bbsid\":    d.Addition.Bbsid,\n\t\t\"folderId\": parent,\n\t\t\"recType\":  \"2\",\n\t}\n\tvar resps ListFileResp\n\t_, err = d.request(\"/pc/resource/getResourceList\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(querys)\n\t}, &resps)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, file := range resps.List {\n\t\t// 手机端超星上传的文件没有fileID字段，但ObjectID与fileID相同，可代替\n\t\tif file.Content.FileID == \"\" {\n\t\t\tfile.Content.FileID = file.Content.ObjectID\n\t\t}\n\t\tfiles = append(files, file)\n\t}\n\treturn files, nil\n}\n\nfunc EncryptByAES(message, key string) (string, error) {\n\taesKey := []byte(key)\n\tplainText := []byte(message)\n\tblock, err := aes.NewCipher(aesKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tiv := aesKey[:aes.BlockSize]\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tpadding := aes.BlockSize - len(plainText)%aes.BlockSize\n\tpaddedText := append(plainText, byte(padding))\n\tfor i := 0; i < padding-1; i++ {\n\t\tpaddedText = append(paddedText, byte(padding))\n\t}\n\tciphertext := make([]byte, len(paddedText))\n\tmode.CryptBlocks(ciphertext, paddedText)\n\tencrypted := base64.StdEncoding.EncodeToString(ciphertext)\n\treturn encrypted, nil\n}\n\nfunc CookiesToString(cookies []*http.Cookie) string {\n\tvar cookieStr string\n\tfor _, cookie := range cookies {\n\t\tcookieStr += cookie.Name + \"=\" + cookie.Value + \"; \"\n\t}\n\tif len(cookieStr) > 2 {\n\t\tcookieStr = cookieStr[:len(cookieStr)-2]\n\t}\n\treturn cookieStr\n}\n\nfunc (d *ChaoXing) Login() (string, error) {\n\ttransferKey := \"u2oh6Vu^HWe4_AES\"\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\tuname, err := EncryptByAES(d.Addition.UserName, transferKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpassword, err := EncryptByAES(d.Addition.Password, transferKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = writer.WriteField(\"uname\", uname)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = writer.WriteField(\"password\", password)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = writer.WriteField(\"t\", \"true\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = writer.Close()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Create the request\n\treq, err := http.NewRequest(http.MethodPost, \"https://passport2.chaoxing.com/fanyalogin\", body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\treq.Header.Set(\"Content-Length\", strconv.Itoa(body.Len()))\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\treturn CookiesToString(resp.Cookies()), nil\n\n}\n"
  },
  {
    "path": "drivers/chunk/driver.go",
    "content": "package chunk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/avast/retry-go\"\n)\n\ntype Chunk struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Chunk) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Chunk) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Chunk) Init(ctx context.Context) error {\n\tif d.PartSize <= 0 {\n\t\treturn errors.New(\"part size must be positive\")\n\t}\n\tif len(d.ChunkPrefix) <= 0 {\n\t\treturn errors.New(\"chunk folder prefix must not be empty\")\n\t}\n\td.RemotePath = utils.FixAndCleanPath(d.RemotePath)\n\treturn nil\n}\n\nfunc (d *Chunk) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (Addition) GetRootPath() string {\n\treturn \"/\"\n}\n\nfunc (d *Chunk) Get(ctx context.Context, path string) (model.Obj, error) {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tremoteActualPath = stdpath.Join(remoteActualPath, path)\n\tif remoteObj, err := op.Get(ctx, remoteStorage, remoteActualPath); err == nil {\n\t\treturn &model.Object{\n\t\t\tPath:     path,\n\t\t\tName:     remoteObj.GetName(),\n\t\t\tSize:     remoteObj.GetSize(),\n\t\t\tModified: remoteObj.ModTime(),\n\t\t\tIsFolder: remoteObj.IsDir(),\n\t\t\tHashInfo: remoteObj.GetHash(),\n\t\t}, nil\n\t}\n\n\tremoteActualDir, name := stdpath.Split(remoteActualPath)\n\tchunkName := d.ChunkPrefix + name\n\tchunkObjs, err := op.List(ctx, remoteStorage, stdpath.Join(remoteActualDir, chunkName), model.ListArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar totalSize int64 = 0\n\t// 0号块默认为-1 以支持空文件\n\tchunkSizes := []int64{-1}\n\th := make(map[*utils.HashType]string)\n\tvar first model.Obj\n\tfor _, o := range chunkObjs {\n\t\tif o.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tif after, ok := strings.CutPrefix(o.GetName(), \"hash_\"); ok {\n\t\t\thn, value, ok := strings.Cut(strings.TrimSuffix(after, d.CustomExt), \"_\")\n\t\t\tif ok {\n\t\t\t\tht, ok := utils.GetHashByName(hn)\n\t\t\t\tif ok {\n\t\t\t\t\th[ht] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tidx, err := strconv.Atoi(strings.TrimSuffix(o.GetName(), d.CustomExt))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\ttotalSize += o.GetSize()\n\t\tif len(chunkSizes) > idx {\n\t\t\tif idx == 0 {\n\t\t\t\tfirst = o\n\t\t\t}\n\t\t\tchunkSizes[idx] = o.GetSize()\n\t\t} else if len(chunkSizes) == idx {\n\t\t\tchunkSizes = append(chunkSizes, o.GetSize())\n\t\t} else {\n\t\t\tnewChunkSizes := make([]int64, idx+1)\n\t\t\tcopy(newChunkSizes, chunkSizes)\n\t\t\tchunkSizes = newChunkSizes\n\t\t\tchunkSizes[idx] = o.GetSize()\n\t\t}\n\t}\n\treqDir, _ := stdpath.Split(path)\n\tobjRes := chunkObject{\n\t\tObject: model.Object{\n\t\t\tPath:     stdpath.Join(reqDir, chunkName),\n\t\t\tName:     name,\n\t\t\tSize:     totalSize,\n\t\t\tModified: first.ModTime(),\n\t\t\tCtime:    first.CreateTime(),\n\t\t},\n\t\tchunkSizes: chunkSizes,\n\t}\n\tif len(h) > 0 {\n\t\tobjRes.HashInfo = utils.NewHashInfoByMap(h)\n\t}\n\treturn &objRes, nil\n}\n\nfunc (d *Chunk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tremoteActualDir := stdpath.Join(remoteActualPath, dir.GetPath())\n\tremoteObjs, err := op.List(ctx, remoteStorage, remoteActualDir, model.ListArgs{\n\t\tReqPath: args.ReqPath,\n\t\tRefresh: args.Refresh,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]model.Obj, 0, len(remoteObjs))\n\tlistG, listCtx := errgroup.NewGroupWithContext(ctx, d.NumListWorkers, retry.Attempts(3))\n\tfor _, obj := range remoteObjs {\n\t\tif utils.IsCanceled(listCtx) {\n\t\t\tbreak\n\t\t}\n\t\trawName := obj.GetName()\n\t\tif obj.IsDir() {\n\t\t\tif name, ok := strings.CutPrefix(rawName, d.ChunkPrefix); ok {\n\t\t\t\tresultIdx := len(result)\n\t\t\t\tresult = append(result, nil)\n\t\t\t\tlistG.Go(func(ctx context.Context) error {\n\t\t\t\t\tchunkObjs, err := op.List(ctx, remoteStorage, stdpath.Join(remoteActualDir, rawName), model.ListArgs{\n\t\t\t\t\t\tReqPath: stdpath.Join(args.ReqPath, rawName),\n\t\t\t\t\t\tRefresh: args.Refresh,\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\ttotalSize := int64(0)\n\t\t\t\t\th := make(map[*utils.HashType]string)\n\t\t\t\t\tfirst := obj\n\t\t\t\t\tfor _, o := range chunkObjs {\n\t\t\t\t\t\tif o.IsDir() {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif after, ok := strings.CutPrefix(strings.TrimSuffix(o.GetName(), d.CustomExt), \"hash_\"); ok {\n\t\t\t\t\t\t\thn, value, ok := strings.Cut(after, \"_\")\n\t\t\t\t\t\t\tif ok {\n\t\t\t\t\t\t\t\tht, ok := utils.GetHashByName(hn)\n\t\t\t\t\t\t\t\tif ok {\n\t\t\t\t\t\t\t\t\th[ht] = value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidx, err := strconv.Atoi(strings.TrimSuffix(o.GetName(), d.CustomExt))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif idx == 0 {\n\t\t\t\t\t\t\tfirst = o\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttotalSize += o.GetSize()\n\t\t\t\t\t}\n\t\t\t\t\tobjRes := model.Object{\n\t\t\t\t\t\tName:     name,\n\t\t\t\t\t\tSize:     totalSize,\n\t\t\t\t\t\tModified: first.ModTime(),\n\t\t\t\t\t\tCtime:    first.CreateTime(),\n\t\t\t\t\t}\n\t\t\t\t\tif len(h) > 0 {\n\t\t\t\t\t\tobjRes.HashInfo = utils.NewHashInfoByMap(h)\n\t\t\t\t\t}\n\t\t\t\t\tif !d.Thumbnail {\n\t\t\t\t\t\tresult[resultIdx] = &objRes\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthumbPath := stdpath.Join(args.ReqPath, \".thumbnails\", name+\".webp\")\n\t\t\t\t\t\tthumb := fmt.Sprintf(\"%s/d%s?sign=%s\",\n\t\t\t\t\t\t\tcommon.GetApiUrl(ctx),\n\t\t\t\t\t\t\tutils.EncodePath(thumbPath, true),\n\t\t\t\t\t\t\tsign.Sign(thumbPath))\n\t\t\t\t\t\tresult[resultIdx] = &model.ObjThumb{\n\t\t\t\t\t\t\tObject: objRes,\n\t\t\t\t\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\t\t\t\t\tThumbnail: thumb,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif !d.ShowHidden && strings.HasPrefix(rawName, \".\") {\n\t\t\tcontinue\n\t\t}\n\t\tthumb, ok := model.GetThumb(obj)\n\t\tobjRes := model.Object{\n\t\t\tName:     rawName,\n\t\t\tSize:     obj.GetSize(),\n\t\t\tModified: obj.ModTime(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t\tHashInfo: obj.GetHash(),\n\t\t}\n\t\tif !ok {\n\t\t\tresult = append(result, &objRes)\n\t\t} else {\n\t\t\tresult = append(result, &model.ObjThumb{\n\t\t\t\tObject: objRes,\n\t\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\t\tThumbnail: thumb,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\tif err = listG.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\nfunc (d *Chunk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tchunkFile, ok := file.(*chunkObject)\n\tremoteActualPath = stdpath.Join(remoteActualPath, file.GetPath())\n\tif !ok {\n\t\tl, _, err := op.Link(ctx, remoteStorage, remoteActualPath, args)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresultLink := *l\n\t\tresultLink.SyncClosers = utils.NewSyncClosers(l)\n\t\treturn &resultLink, nil\n\t}\n\t// 检查0号块不等于-1 以支持空文件\n\t// 如果块数量大于1 最后一块不可能为0\n\t// 只检查中间块是否有0\n\tif chunkFile.chunkSizes[0] == -1 {\n\t\treturn nil, fmt.Errorf(\"chunk part[%d] are missing\", 0)\n\t}\n\tfor i, l := 1, len(chunkFile.chunkSizes)-1; i < l; i++ {\n\t\tif chunkFile.chunkSizes[i] == 0 {\n\t\t\treturn nil, fmt.Errorf(\"chunk part[%d] are missing\", i)\n\t\t}\n\t}\n\tfileSize := chunkFile.GetSize()\n\tmergedRrf := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tstart := httpRange.Start\n\t\tlength := httpRange.Length\n\t\tif length < 0 || start+length > fileSize {\n\t\t\tlength = fileSize - start\n\t\t}\n\t\tif length == 0 {\n\t\t\treturn io.NopCloser(strings.NewReader(\"\")), nil\n\t\t}\n\t\trs := make([]io.Reader, 0)\n\t\tcs := make(utils.Closers, 0)\n\t\tvar (\n\t\t\trc       io.ReadCloser\n\t\t\treadFrom bool\n\t\t)\n\t\tfor idx, chunkSize := range chunkFile.chunkSizes {\n\t\t\tif readFrom {\n\t\t\t\tl, o, err := op.Link(ctx, remoteStorage, stdpath.Join(remoteActualPath, d.getPartName(idx)), args)\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcs = append(cs, l)\n\t\t\t\tchunkSize2 := l.ContentLength\n\t\t\t\tif chunkSize2 <= 0 {\n\t\t\t\t\tchunkSize2 = o.GetSize()\n\t\t\t\t}\n\t\t\t\tif chunkSize2 != chunkSize {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, fmt.Errorf(\"chunk part[%d] size not match\", idx)\n\t\t\t\t}\n\t\t\t\trrf, err := stream.GetRangeReaderFromLink(chunkSize2, l)\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tnewLength := length - chunkSize2\n\t\t\t\tif newLength >= 0 {\n\t\t\t\t\tlength = newLength\n\t\t\t\t\trc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1})\n\t\t\t\t} else {\n\t\t\t\t\trc, err = rrf.RangeRead(ctx, http_range.Range{Length: length})\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\trs = append(rs, rc)\n\t\t\t\tcs = append(cs, rc)\n\t\t\t\tif newLength <= 0 {\n\t\t\t\t\treturn utils.ReadCloser{\n\t\t\t\t\t\tReader: io.MultiReader(rs...),\n\t\t\t\t\t\tCloser: &cs,\n\t\t\t\t\t}, nil\n\t\t\t\t}\n\t\t\t} else if newStart := start - chunkSize; newStart >= 0 {\n\t\t\t\tstart = newStart\n\t\t\t} else {\n\t\t\t\tl, o, err := op.Link(ctx, remoteStorage, stdpath.Join(remoteActualPath, d.getPartName(idx)), args)\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcs = append(cs, l)\n\t\t\t\tchunkSize2 := l.ContentLength\n\t\t\t\tif chunkSize2 <= 0 {\n\t\t\t\t\tchunkSize2 = o.GetSize()\n\t\t\t\t}\n\t\t\t\tif chunkSize2 != chunkSize {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, fmt.Errorf(\"chunk part[%d] size not match\", idx)\n\t\t\t\t}\n\t\t\t\trrf, err := stream.GetRangeReaderFromLink(chunkSize2, l)\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\trc, err = rrf.RangeRead(ctx, http_range.Range{Start: start, Length: -1})\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = cs.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tlength -= chunkSize2 - start\n\t\t\t\tcs = append(cs, rc)\n\t\t\t\tif length <= 0 {\n\t\t\t\t\treturn utils.ReadCloser{\n\t\t\t\t\t\tReader: rc,\n\t\t\t\t\t\tCloser: &cs,\n\t\t\t\t\t}, nil\n\t\t\t\t}\n\t\t\t\trs = append(rs, rc)\n\t\t\t\treadFrom = true\n\t\t\t}\n\t\t}\n\t\treturn nil, fmt.Errorf(\"invalid range: start=%d,length=%d,fileSize=%d\", httpRange.Start, httpRange.Length, fileSize)\n\t}\n\treturn &model.Link{\n\t\tRangeReader: stream.RangeReaderFunc(mergedRrf),\n\t}, nil\n}\n\nfunc (d *Chunk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tpath := stdpath.Join(d.RemotePath, parentDir.GetPath(), dirName)\n\treturn fs.MakeDir(ctx, path)\n}\n\nfunc (d *Chunk) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrc := stdpath.Join(d.RemotePath, srcObj.GetPath())\n\tdst := stdpath.Join(d.RemotePath, dstDir.GetPath())\n\t_, err := fs.Move(ctx, src, dst)\n\treturn err\n}\n\nfunc (d *Chunk) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif _, ok := srcObj.(*chunkObject); ok {\n\t\tnewName = d.ChunkPrefix + newName\n\t}\n\treturn fs.Rename(ctx, stdpath.Join(d.RemotePath, srcObj.GetPath()), newName)\n}\n\nfunc (d *Chunk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdst := stdpath.Join(d.RemotePath, dstDir.GetPath())\n\tsrc := stdpath.Join(d.RemotePath, srcObj.GetPath())\n\t_, err := fs.Copy(ctx, src, dst)\n\treturn err\n}\n\nfunc (d *Chunk) Remove(ctx context.Context, obj model.Obj) error {\n\treturn fs.Remove(ctx, stdpath.Join(d.RemotePath, obj.GetPath()))\n}\n\nfunc (d *Chunk) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif (d.Thumbnail && dstDir.GetName() == \".thumbnails\") || (d.ChunkLargeFileOnly && file.GetSize() <= d.PartSize) {\n\t\treturn op.Put(ctx, remoteStorage, stdpath.Join(remoteActualPath, dstDir.GetPath()), file, up)\n\t}\n\tupReader := &driver.ReaderUpdatingProgress{\n\t\tReader:         file,\n\t\tUpdateProgress: up,\n\t}\n\tdst := stdpath.Join(remoteActualPath, dstDir.GetPath(), d.ChunkPrefix+file.GetName())\n\tskipHookCtx := context.WithValue(ctx, conf.SkipHookKey, struct{}{})\n\tif d.StoreHash {\n\t\tfor ht, value := range file.GetHash().All() {\n\t\t\t_ = op.Put(skipHookCtx, remoteStorage, dst, &stream.FileStream{\n\t\t\t\tObj: &model.Object{\n\t\t\t\t\tName:     fmt.Sprintf(\"hash_%s_%s%s\", ht.Name, value, d.CustomExt),\n\t\t\t\t\tSize:     1,\n\t\t\t\t\tModified: file.ModTime(),\n\t\t\t\t},\n\t\t\t\tMimetype: \"application/octet-stream\",\n\t\t\t\tReader:   bytes.NewReader([]byte{0}), // 兼容不支持空文件的驱动\n\t\t\t}, nil)\n\t\t}\n\t}\n\tfullPartCount := int(file.GetSize() / d.PartSize)\n\ttailSize := file.GetSize() % d.PartSize\n\tif tailSize == 0 && fullPartCount > 0 {\n\t\tfullPartCount--\n\t\ttailSize = d.PartSize\n\t}\n\tpartIndex := 0\n\tfor partIndex < fullPartCount {\n\t\terr = op.Put(skipHookCtx, remoteStorage, dst, &stream.FileStream{\n\t\t\tObj: &model.Object{\n\t\t\t\tName:     d.getPartName(partIndex),\n\t\t\t\tSize:     d.PartSize,\n\t\t\t\tModified: file.ModTime(),\n\t\t\t},\n\t\t\tMimetype: file.GetMimetype(),\n\t\t\tReader:   io.LimitReader(upReader, d.PartSize),\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\t_ = op.Remove(ctx, remoteStorage, dst)\n\t\t\treturn err\n\t\t}\n\t\tpartIndex++\n\t}\n\terr = op.Put(ctx, remoteStorage, dst, &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tName:     d.getPartName(fullPartCount),\n\t\t\tSize:     tailSize,\n\t\t\tModified: file.ModTime(),\n\t\t},\n\t\tMimetype: file.GetMimetype(),\n\t\tReader:   upReader,\n\t}, nil)\n\tif err != nil {\n\t\t_ = op.Remove(ctx, remoteStorage, dst)\n\t}\n\treturn err\n}\n\nfunc (d *Chunk) getPartName(part int) string {\n\treturn fmt.Sprintf(\"%d%s\", part, d.CustomExt)\n}\n\nfunc (d *Chunk) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tremoteStorage, err := fs.GetStorage(d.RemotePath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\treturn nil, errs.NotImplement\n\t}\n\tremoteDetails, err := op.GetStorageDetails(ctx, remoteStorage)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: remoteDetails.DiskUsage,\n\t}, nil\n}\n\nvar _ driver.Driver = (*Chunk)(nil)\n"
  },
  {
    "path": "drivers/chunk/meta.go",
    "content": "package chunk\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tRemotePath         string `json:\"remote_path\" required:\"true\"`\n\tPartSize           int64  `json:\"part_size\" required:\"true\" type:\"number\" help:\"bytes\"`\n\tChunkLargeFileOnly bool   `json:\"chunk_large_file_only\" default:\"false\" help:\"chunk only if file size > part_size\"`\n\tChunkPrefix        string `json:\"chunk_prefix\" type:\"string\" default:\"[openlist_chunk]\" help:\"the prefix of chunk folder\"`\n\tCustomExt          string `json:\"custom_ext\" type:\"string\"`\n\tStoreHash          bool   `json:\"store_hash\" type:\"bool\" default:\"true\"`\n\tNumListWorkers     int    `json:\"num_list_workers\" required:\"true\" type:\"number\" default:\"5\"`\n\n\tThumbnail  bool `json:\"thumbnail\" required:\"true\" default:\"false\" help:\"enable thumbnail which pre-generated under .thumbnails folder\"`\n\tShowHidden bool `json:\"show_hidden\"  default:\"true\" required:\"false\" help:\"show hidden directories and files\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Chunk\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tNoCache:     true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Chunk{\n\t\t\tAddition: Addition{\n\t\t\t\tChunkPrefix:    \"[openlist_chunk]\",\n\t\t\t\tNumListWorkers: 5,\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/chunk/obj.go",
    "content": "package chunk\n\nimport \"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\ntype chunkObject struct {\n\tmodel.Object\n\tchunkSizes []int64\n}\n"
  },
  {
    "path": "drivers/cloudreve/driver.go",
    "content": "package cloudreve\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Cloudreve struct {\n\tmodel.Storage\n\tAddition\n\tref *Cloudreve\n}\n\nfunc (d *Cloudreve) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Cloudreve) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Cloudreve) Init(ctx context.Context) error {\n\tif d.Cookie != \"\" {\n\t\treturn nil\n\t}\n\t// removing trailing slash\n\td.Address = strings.TrimSuffix(d.Address, \"/\")\n\treturn d.login()\n}\n\nfunc (d *Cloudreve) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*Cloudreve)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *Cloudreve) Drop(ctx context.Context) error {\n\td.Cookie = \"\"\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *Cloudreve) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar r DirectoryResp\n\terr := d.request(http.MethodGet, \"/directory\"+dir.GetPath(), nil, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.SliceConvert(r.Objects, func(src Object) (model.Obj, error) {\n\t\tthumb, err := d.GetThumb(src)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif src.Type == \"dir\" && d.EnableThumbAndFolderSize {\n\t\t\tvar dprop DirectoryProp\n\t\t\terr = d.request(http.MethodGet, \"/object/property/\"+src.Id+\"?is_folder=true\", nil, &dprop)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsrc.Size = dprop.Size\n\t\t}\n\t\tsrc.Path = path.Join(dir.GetPath(), src.Name)\n\t\treturn objectToObj(src, thumb), nil\n\t})\n}\n\nfunc (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar dUrl string\n\terr := d.request(http.MethodPut, \"/file/download/\"+file.GetID(), nil, &dUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif strings.HasPrefix(dUrl, \"/api\") {\n\t\tdUrl = d.Address + dUrl\n\t}\n\treturn &model.Link{\n\t\tURL: dUrl,\n\t}, nil\n}\n\nfunc (d *Cloudreve) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.request(http.MethodPut, \"/directory\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"path\": parentDir.GetPath() + \"/\" + dirName,\n\t\t})\n\t}, nil)\n}\n\nfunc (d *Cloudreve) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tbody := base.Json{\n\t\t\"action\":  \"move\",\n\t\t\"src_dir\": path.Dir(srcObj.GetPath()),\n\t\t\"dst\":     dstDir.GetPath(),\n\t\t\"src\":     convertSrc(srcObj),\n\t}\n\treturn d.request(http.MethodPatch, \"/object\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Cloudreve) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tbody := base.Json{\n\t\t\"action\":   \"rename\",\n\t\t\"new_name\": newName,\n\t\t\"src\":      convertSrc(srcObj),\n\t}\n\treturn d.request(http.MethodPatch, \"/object/rename\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Cloudreve) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tbody := base.Json{\n\t\t\"src_dir\": path.Dir(srcObj.GetPath()),\n\t\t\"dst\":     dstDir.GetPath(),\n\t\t\"src\":     convertSrc(srcObj),\n\t}\n\treturn d.request(http.MethodPost, \"/object/copy\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Cloudreve) Remove(ctx context.Context, obj model.Obj) error {\n\tbody := convertSrc(obj)\n\terr := d.request(http.MethodDelete, \"/object\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif io.ReadCloser(stream) == http.NoBody {\n\t\treturn d.create(ctx, dstDir, stream)\n\t}\n\n\t// 获取存储策略\n\tvar r DirectoryResp\n\terr := d.request(http.MethodGet, \"/directory\"+dstDir.GetPath(), nil, &r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuploadBody := base.Json{\n\t\t\"path\":          dstDir.GetPath(),\n\t\t\"size\":          stream.GetSize(),\n\t\t\"name\":          stream.GetName(),\n\t\t\"policy_id\":     r.Policy.Id,\n\t\t\"last_modified\": stream.ModTime().UnixMilli(),\n\t}\n\n\t// 获取上传会话信息\n\tvar u UploadInfo\n\terr = d.request(http.MethodPut, \"/file/upload\", func(req *resty.Request) {\n\t\treq.SetBody(uploadBody)\n\t}, &u)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 根据存储方式选择分片上传的方法\n\tswitch r.Policy.Type {\n\tcase \"onedrive\":\n\t\terr = d.upOneDrive(ctx, stream, u, up)\n\tcase \"s3\":\n\t\terr = d.upS3(ctx, stream, u, up)\n\tcase \"remote\": // 从机存储\n\t\terr = d.upRemote(ctx, stream, u, up)\n\tcase \"local\": // 本机存储\n\t\terr = d.upLocal(ctx, stream, u, up)\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\tif err != nil {\n\t\t// 删除失败的会话\n\t\t_ = d.request(http.MethodDelete, \"/file/upload/\"+u.SessionID, nil, nil)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error {\n\tbody := base.Json{\"path\": dir.GetPath() + \"/\" + file.GetName()}\n\tif file.IsDir() {\n\t\terr := d.request(http.MethodPut, \"directory\", func(req *resty.Request) {\n\t\t\treq.SetBody(body)\n\t\t}, nil)\n\t\treturn err\n\t}\n\treturn d.request(http.MethodPost, \"/file/create\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Cloudreve) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tvar r StorageDetails\n\td.request(http.MethodGet, \"/user/storage\", nil, &r)\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: r.Total,\n\t\t\tUsedSpace:  r.Used,\n\t\t},\n\t}, nil\n}\n\n//func (d *Cloudreve) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Cloudreve)(nil)\n"
  },
  {
    "path": "drivers/cloudreve/meta.go",
    "content": "package cloudreve\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// define other\n\tAddress                  string `json:\"address\" required:\"true\"`\n\tUsername                 string `json:\"username\"`\n\tPassword                 string `json:\"password\"`\n\tCookie                   string `json:\"cookie\"`\n\tCustomUA                 string `json:\"custom_ua\"`\n\tEnableThumbAndFolderSize bool   `json:\"enable_thumb_and_folder_size\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Cloudreve\",\n\tDefaultRoot: \"/\",\n\tLocalSort:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Cloudreve{}\n\t})\n}\n"
  },
  {
    "path": "drivers/cloudreve/types.go",
    "content": "package cloudreve\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Resp struct {\n\tCode int         `json:\"code\"`\n\tMsg  string      `json:\"msg\"`\n\tData interface{} `json:\"data\"`\n}\n\ntype Policy struct {\n\tId       string   `json:\"id\"`\n\tName     string   `json:\"name\"`\n\tType     string   `json:\"type\"`\n\tMaxSize  int      `json:\"max_size\"`\n\tFileType []string `json:\"file_type\"`\n}\n\ntype UploadInfo struct {\n\tSessionID   string   `json:\"sessionID\"`\n\tChunkSize   int      `json:\"chunkSize\"`\n\tExpires     int      `json:\"expires\"`\n\tUploadURLs  []string `json:\"uploadURLs\"`\n\tCredential  string   `json:\"credential,omitempty\"`  // local\n\tCompleteURL string   `json:\"completeURL,omitempty\"` // s3\n}\n\ntype DirectoryResp struct {\n\tParent  string   `json:\"parent\"`\n\tObjects []Object `json:\"objects\"`\n\tPolicy  Policy   `json:\"policy\"`\n}\n\ntype Object struct {\n\tId            string    `json:\"id\"`\n\tName          string    `json:\"name\"`\n\tPath          string    `json:\"path\"`\n\tPic           string    `json:\"pic\"`\n\tSize          int       `json:\"size\"`\n\tType          string    `json:\"type\"`\n\tDate          time.Time `json:\"date\"`\n\tCreateDate    time.Time `json:\"create_date\"`\n\tSourceEnabled bool      `json:\"source_enabled\"`\n}\n\ntype DirectoryProp struct {\n\tSize int `json:\"size\"`\n}\n\nfunc objectToObj(f Object, t model.Thumbnail) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Id,\n\t\t\tName:     f.Name,\n\t\t\tSize:     int64(f.Size),\n\t\t\tModified: f.Date,\n\t\t\tIsFolder: f.Type == \"dir\",\n\t\t\tPath:     f.Path,\n\t\t},\n\t\tThumbnail: t,\n\t}\n}\n\ntype Config struct {\n\tLoginCaptcha bool   `json:\"loginCaptcha\"`\n\tCaptchaType  string `json:\"captcha_type\"`\n}\n\ntype StorageDetails struct {\n\tUsed  int64 `json:\"used\"`\n\tFree  int64 `json:\"free\"`\n\tTotal int64 `json:\"total\"`\n}\n"
  },
  {
    "path": "drivers/cloudreve/util.go",
    "content": "package cloudreve\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cookie\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// do others that not defined in Driver interface\n\nconst loginPath = \"/user/session\"\n\nfunc (d *Cloudreve) getUA() string {\n\tif d.CustomUA != \"\" {\n\t\treturn d.CustomUA\n\t}\n\treturn base.UserAgent\n}\n\nfunc (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {\n\tif d.ref != nil {\n\t\treturn d.ref.request(method, path, callback, out)\n\t}\n\tu := d.Address + \"/api/v3\" + path\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":     \"cloudreve-session=\" + d.Cookie,\n\t\t\"Accept\":     \"application/json, text/plain, */*\",\n\t\t\"User-Agent\": d.getUA(),\n\t})\n\n\tvar r Resp\n\treq.SetResult(&r)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tresp, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !resp.IsSuccess() {\n\t\treturn errors.New(resp.String())\n\t}\n\n\tif r.Code != 0 {\n\n\t\t// 刷新 cookie\n\t\tif r.Code == http.StatusUnauthorized && path != loginPath {\n\t\t\tif d.Username != \"\" && d.Password != \"\" {\n\t\t\t\terr = d.login()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn d.request(method, path, callback, out)\n\t\t\t}\n\t\t}\n\n\t\treturn errors.New(r.Msg)\n\t}\n\tsess := cookie.GetCookie(resp.Cookies(), \"cloudreve-session\")\n\tif sess != nil {\n\t\td.Cookie = sess.Value\n\t}\n\tif out != nil && r.Data != nil {\n\t\tvar marshal []byte\n\t\tmarshal, err = jsoniter.Marshal(r.Data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = jsoniter.Unmarshal(marshal, out)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Cloudreve) login() error {\n\tvar siteConfig Config\n\terr := d.request(http.MethodGet, \"/site/config\", nil, &siteConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor i := 0; i < 5; i++ {\n\t\terr = d.doLogin(siteConfig.LoginCaptcha)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif err.Error() != \"CAPTCHA not match.\" {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Cloudreve) doLogin(needCaptcha bool) error {\n\tvar captchaCode string\n\tvar err error\n\tif needCaptcha {\n\t\tvar captcha string\n\t\terr = d.request(http.MethodGet, \"/site/captcha\", nil, &captcha)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(captcha) == 0 {\n\t\t\treturn errors.New(\"can not get captcha\")\n\t\t}\n\t\ti := strings.Index(captcha, \",\")\n\t\tdec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha[i+1:]))\n\t\tvRes, err := base.RestyClient.R().SetMultipartField(\n\t\t\t\"image\", \"validateCode.png\", \"image/png\", dec).\n\t\t\tPost(setting.GetStr(conf.OcrApi))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif jsoniter.Get(vRes.Body(), \"status\").ToInt() != 200 {\n\t\t\treturn errors.New(\"ocr error:\" + jsoniter.Get(vRes.Body(), \"msg\").ToString())\n\t\t}\n\t\tcaptchaCode = jsoniter.Get(vRes.Body(), \"result\").ToString()\n\t}\n\tvar resp Resp\n\terr = d.request(http.MethodPost, loginPath, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"username\":    d.Addition.Username,\n\t\t\t\"Password\":    d.Addition.Password,\n\t\t\t\"captchaCode\": captchaCode,\n\t\t})\n\t}, &resp)\n\treturn err\n}\n\nfunc convertSrc(obj model.Obj) map[string]interface{} {\n\tm := make(map[string]interface{})\n\tvar dirs []string\n\tvar items []string\n\tif obj.IsDir() {\n\t\tdirs = append(dirs, obj.GetID())\n\t} else {\n\t\titems = append(items, obj.GetID())\n\t}\n\tm[\"dirs\"] = dirs\n\tm[\"items\"] = items\n\treturn m\n}\n\nfunc (d *Cloudreve) GetThumb(file Object) (model.Thumbnail, error) {\n\tif !d.Addition.EnableThumbAndFolderSize {\n\t\treturn model.Thumbnail{}, nil\n\t}\n\treq := base.NoRedirectClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":     \"cloudreve-session=\" + d.Cookie,\n\t\t\"Accept\":     \"image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\",\n\t\t\"User-Agent\": d.getUA(),\n\t})\n\tresp, err := req.Execute(http.MethodGet, d.Address+\"/api/v3/file/thumb/\"+file.Id)\n\tif err != nil {\n\t\treturn model.Thumbnail{}, err\n\t}\n\treturn model.Thumbnail{\n\t\tThumbnail: resp.Header().Get(\"Location\"),\n\t}, nil\n}\n\nfunc (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[Cloudreve-Local] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\tbyteData := make([]byte, byteSize)\n\t\tn, err := io.ReadFull(stream, byteData)\n\t\tutils.Log.Debug(err, n)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = d.request(http.MethodPost, \"/file/upload/\"+u.SessionID+\"/\"+strconv.Itoa(chunk), func(req *resty.Request) {\n\t\t\treq.SetHeader(\"Content-Type\", \"application/octet-stream\")\n\t\t\treq.SetContentLength(true)\n\t\t\treq.SetHeader(\"Content-Length\", strconv.FormatInt(byteSize, 10))\n\t\t\treq.SetHeader(\"User-Agent\", d.getUA())\n\t\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\t\treq.AddRetryCondition(func(r *resty.Response, err error) bool {\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif r.IsError() {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tvar retryResp Resp\n\t\t\t\tjErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)\n\t\t\t\tif jErr != nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif retryResp.Code != 0 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\tchunk++\n\t}\n\treturn nil\n}\n\nfunc (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := u.UploadURLs[0]\n\tcredential := u.Credential\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[Cloudreve-Remote] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl+\"?chunk=\"+strconv.Itoa(chunk),\n\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Authorization\", fmt.Sprint(credential))\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\tif res.StatusCode != 200 {\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\t}\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tvar up Resp\n\t\t\t\terr = json.Unmarshal(body, &up)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif up.Code != 0 {\n\t\t\t\t\treturn errors.New(up.Msg)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\tchunk++\n\t}\n\treturn nil\n}\n\nfunc (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := u.UploadURLs[0]\n\tvar finish int64 = 0\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[Cloudreve-OneDrive] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\t\t\treturn errors.New(string(data))\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t}\n\t// 上传成功发送回调请求\n\treturn d.request(http.MethodPost, \"/callback/onedrive/finish/\"+u.SessionID, func(req *resty.Request) {\n\t\treq.SetBody(\"{}\")\n\t}, nil)\n}\n\nfunc (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tvar etags []string\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[Cloudreve-S3] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, u.UploadURLs[chunk],\n\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tetag := res.Header.Get(\"ETag\")\n\t\t\t\tres.Body.Close()\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode != 200:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase etag == \"\":\n\t\t\t\t\treturn errors.New(\"failed to get ETag from header\")\n\t\t\t\tdefault:\n\t\t\t\t\tetags = append(etags, etag)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\tchunk++\n\t}\n\t// s3LikeFinishUpload\n\t// https://github.com/cloudreve/frontend/blob/b485bf297974cbe4834d2e8e744ae7b7e5b2ad39/src/component/Uploader/core/api/index.ts#L204-L252\n\tbodyBuilder := &strings.Builder{}\n\tbodyBuilder.WriteString(\"<CompleteMultipartUpload>\")\n\tfor i, etag := range etags {\n\t\tbodyBuilder.WriteString(fmt.Sprintf(\n\t\t\t`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,\n\t\t\ti+1, // PartNumber 从 1 开始\n\t\t\tetag,\n\t\t))\n\t}\n\tbodyBuilder.WriteString(\"</CompleteMultipartUpload>\")\n\treq, err := http.NewRequestWithContext(ctx,\n\t\thttp.MethodPost,\n\t\tu.CompleteURL,\n\t\tstrings.NewReader(bodyBuilder.String()),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/xml\")\n\treq.Header.Set(\"User-Agent\", d.getUA())\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(res.Body)\n\t\treturn fmt.Errorf(\"up status: %d, error: %s\", res.StatusCode, string(body))\n\t}\n\n\t// 上传成功发送回调请求\n\terr = d.request(http.MethodGet, \"/callback/s3/\"+u.SessionID, nil, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/cloudreve_v4/driver.go",
    "content": "package cloudreve_v4\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype CloudreveV4 struct {\n\tmodel.Storage\n\tAddition\n\tref            *CloudreveV4\n\tAccessExpires  string\n\tRefreshExpires string\n}\n\nfunc (d *CloudreveV4) Config() driver.Config {\n\tif d.ref != nil {\n\t\treturn d.ref.Config()\n\t}\n\tif d.EnableVersionUpload {\n\t\tconfig.NoOverwriteUpload = false\n\t}\n\treturn config\n}\n\nfunc (d *CloudreveV4) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *CloudreveV4) Init(ctx context.Context) error {\n\t// removing trailing slash\n\td.Address = strings.TrimSuffix(d.Address, \"/\")\n\top.MustSaveDriverStorage(d)\n\tif d.ref != nil {\n\t\treturn nil\n\t}\n\tif d.canLogin() {\n\t\treturn d.login()\n\t}\n\tif d.RefreshToken != \"\" {\n\t\treturn d.refreshToken()\n\t}\n\tif d.AccessToken == \"\" {\n\t\treturn errors.New(\"no way to authenticate. At least AccessToken is required\")\n\t}\n\t// ensure AccessToken is valid\n\treturn d.parseJWT(d.AccessToken, &AccessJWT{})\n}\n\nfunc (d *CloudreveV4) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*CloudreveV4)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *CloudreveV4) Drop(ctx context.Context) error {\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *CloudreveV4) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tconst pageSize int = 100\n\tvar f []File\n\tvar r FileResp\n\tparams := map[string]string{\n\t\t\"page_size\":       strconv.Itoa(pageSize),\n\t\t\"uri\":             dir.GetPath(),\n\t\t\"order_by\":        d.OrderBy,\n\t\t\"order_direction\": d.OrderDirection,\n\t\t\"page\":            \"0\",\n\t}\n\n\tfor {\n\t\terr := d.request(http.MethodGet, \"/file\", func(req *resty.Request) {\n\t\t\treq.SetQueryParams(params)\n\t\t}, &r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tf = append(f, r.Files...)\n\t\tif r.Pagination.NextToken == \"\" || len(r.Files) < pageSize {\n\t\t\tbreak\n\t\t}\n\t\tparams[\"next_page_token\"] = r.Pagination.NextToken\n\t}\n\n\tif d.HideUploading {\n\t\tf = utils.SliceFilter(f, func(src File) bool {\n\t\t\treturn src.Metadata == nil || src.Metadata[MetadataUploadSessionID] == nil\n\t\t})\n\t}\n\n\treturn utils.SliceConvert(f, func(src File) (model.Obj, error) {\n\t\tif d.EnableFolderSize && src.Type == 1 {\n\t\t\tvar ds FolderSummaryResp\n\t\t\terr := d.request(http.MethodGet, \"/file/info\", func(req *resty.Request) {\n\t\t\t\treq.SetQueryParam(\"uri\", src.Path)\n\t\t\t\treq.SetQueryParam(\"folder_summary\", \"true\")\n\t\t\t}, &ds)\n\t\t\tif err == nil && ds.FolderSummary.Size > 0 {\n\t\t\t\tsrc.Size = ds.FolderSummary.Size\n\t\t\t}\n\t\t}\n\t\tvar thumb model.Thumbnail\n\t\tif d.EnableThumb && src.Type == 0 && (src.Metadata == nil || src.Metadata[MetadataThumbDisabled] == \"\") {\n\t\t\tvar t FileThumbResp\n\t\t\terr := d.request(http.MethodGet, \"/file/thumb\", func(req *resty.Request) {\n\t\t\t\treq.SetQueryParam(\"uri\", src.Path)\n\t\t\t}, &t)\n\t\t\tif err == nil && t.URL != \"\" {\n\t\t\t\tthumb = model.Thumbnail{\n\t\t\t\t\tThumbnail: t.URL,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &model.ObjThumb{\n\t\t\tObject:    *fileToObject(&src),\n\t\t\tThumbnail: thumb,\n\t\t}, nil\n\t})\n}\n\nfunc (d *CloudreveV4) Get(ctx context.Context, path string) (model.Obj, error) {\n\tvar info File\n\terr := d.request(http.MethodGet, \"/file/info\", func(req *resty.Request) {\n\t\treq.SetQueryParam(\"uri\", d.RootFolderPath+path)\n\t}, &info)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fileToObject(&info), nil\n}\n\nfunc (d *CloudreveV4) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar url FileUrlResp\n\terr := d.request(http.MethodPost, \"/file/url\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"uris\":     []string{file.GetPath()},\n\t\t\t\"download\": true,\n\t\t})\n\t}, &url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(url.Urls) == 0 {\n\t\treturn nil, errors.New(\"server returns no url\")\n\t}\n\texp := time.Until(url.Expires)\n\treturn &model.Link{\n\t\tURL:        url.Urls[0].URL,\n\t\tExpiration: &exp,\n\t}, nil\n}\n\nfunc (d *CloudreveV4) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.request(http.MethodPost, \"/file/create\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"type\":              \"folder\",\n\t\t\t\"uri\":               parentDir.GetPath() + \"/\" + dirName,\n\t\t\t\"error_on_conflict\": true,\n\t\t})\n\t}, nil)\n}\n\nfunc (d *CloudreveV4) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.request(http.MethodPost, \"/file/move\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"uris\": []string{srcObj.GetPath()},\n\t\t\t\"dst\":  dstDir.GetPath(),\n\t\t\t\"copy\": false,\n\t\t})\n\t}, nil)\n}\n\nfunc (d *CloudreveV4) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn d.request(http.MethodPost, \"/file/rename\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"new_name\": newName,\n\t\t\t\"uri\":      srcObj.GetPath(),\n\t\t})\n\t}, nil)\n}\n\nfunc (d *CloudreveV4) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.request(http.MethodPost, \"/file/move\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"uris\": []string{srcObj.GetPath()},\n\t\t\t\"dst\":  dstDir.GetPath(),\n\t\t\t\"copy\": true,\n\t\t})\n\t}, nil)\n}\n\nfunc (d *CloudreveV4) Remove(ctx context.Context, obj model.Obj) error {\n\tvar r FileDeleteResp\n\terr := d.request(http.MethodDelete, \"/file\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"uris\":             []string{obj.GetPath()},\n\t\t\t\"unlink\":           false,\n\t\t\t\"skip_soft_delete\": true,\n\t\t})\n\t\treq.SetResult(&r)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif r.Code == 0 {\n\t\treturn nil\n\t}\n\tif r.Code == 40073 && r.Msg == \"Lock conflict\" && len(r.Data) > 0 {\n\t\ttokens := make([]string, 0, len(r.Data))\n\t\tfor _, item := range r.Data {\n\t\t\ttokens = append(tokens, item.Token)\n\t\t}\n\t\terr = d.request(http.MethodDelete, \"/file/lock\", func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"tokens\": tokens,\n\t\t\t})\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn d.request(http.MethodDelete, \"/file\", func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"uris\":             []string{obj.GetPath()},\n\t\t\t\t\"unlink\":           false,\n\t\t\t\t\"skip_soft_delete\": true,\n\t\t\t})\n\t\t}, nil)\n\t}\n\treturn errors.New(r.Msg)\n}\n\nfunc (d *CloudreveV4) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tif file.GetSize() == 0 {\n\t\t// 空文件使用新建文件方法，避免上传卡锁\n\t\treturn d.request(http.MethodPost, \"/file/create\", func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"type\":              \"file\",\n\t\t\t\t\"uri\":               dstDir.GetPath() + \"/\" + file.GetName(),\n\t\t\t\t\"error_on_conflict\": true,\n\t\t\t})\n\t\t}, nil)\n\t}\n\tvar p StoragePolicy\n\tvar r FileResp\n\tvar u FileUploadResp\n\tvar err error\n\tparams := map[string]string{\n\t\t\"page_size\":       \"10\",\n\t\t\"uri\":             dstDir.GetPath(),\n\t\t\"order_by\":        \"created_at\",\n\t\t\"order_direction\": \"asc\",\n\t\t\"page\":            \"0\",\n\t}\n\terr = d.request(http.MethodGet, \"/file\", func(req *resty.Request) {\n\t\treq.SetQueryParams(params)\n\t}, &r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp = r.StoragePolicy\n\tbody := base.Json{\n\t\t\"uri\":           dstDir.GetPath() + \"/\" + file.GetName(),\n\t\t\"size\":          file.GetSize(),\n\t\t\"policy_id\":     p.ID,\n\t\t\"last_modified\": file.ModTime().UnixMilli(),\n\t\t\"mime_type\":     \"\",\n\t}\n\tif d.EnableVersionUpload {\n\t\tbody[\"entity_type\"] = \"version\"\n\t}\n\terr = d.request(http.MethodPut, \"/file/upload\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, &u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif u.StoragePolicy.Relay {\n\t\terr = d.upLocal(ctx, file, u, up)\n\t} else {\n\t\tswitch u.StoragePolicy.Type {\n\t\tcase \"local\":\n\t\t\terr = d.upLocal(ctx, file, u, up)\n\t\tcase \"remote\":\n\t\t\terr = d.upRemote(ctx, file, u, up)\n\t\tcase \"onedrive\":\n\t\t\terr = d.upOneDrive(ctx, file, u, up)\n\t\tcase \"s3\":\n\t\t\terr = d.upS3(ctx, file, u, up, \"s3\")\n\t\tcase \"ks3\":\n\t\t\terr = d.upS3(ctx, file, u, up, \"ks3\")\n\t\tdefault:\n\t\t\treturn errs.NotImplement\n\t\t}\n\t}\n\tif err != nil {\n\t\t// 删除失败的会话\n\t\t_ = d.request(http.MethodDelete, \"/file/upload\", func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"id\":  u.SessionID,\n\t\t\t\t\"uri\": u.URI,\n\t\t\t})\n\t\t}, nil)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *CloudreveV4) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CloudreveV4) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CloudreveV4) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CloudreveV4) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n\t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n\t// return errs.NotImplement to use an internal archive tool\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CloudreveV4) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\t// TODO return storage details (total space, free space, etc.)\n\tvar r CapacityResp\n\terr := d.request(http.MethodGet, \"/user/capacity\", func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: r.Total,\n\t\t\tUsedSpace:  r.Used,\n\t\t},\n\t}, nil\n}\n\n//func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*CloudreveV4)(nil)\n"
  },
  {
    "path": "drivers/cloudreve_v4/meta.go",
    "content": "package cloudreve_v4\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// driver.RootID\n\t// define other\n\tAddress             string `json:\"address\" required:\"true\"`\n\tUsername            string `json:\"username\"`\n\tPassword            string `json:\"password\"`\n\tAccessToken         string `json:\"access_token\"`\n\tRefreshToken        string `json:\"refresh_token\"`\n\tCustomUA            string `json:\"custom_ua\"`\n\tEnableFolderSize    bool   `json:\"enable_folder_size\"`\n\tEnableThumb         bool   `json:\"enable_thumb\"`\n\tEnableVersionUpload bool   `json:\"enable_version_upload\"`\n\tHideUploading       bool   `json:\"hide_uploading\"`\n\tOrderBy             string `json:\"order_by\" type:\"select\" options:\"name,size,updated_at,created_at\" default:\"name\" required:\"true\"`\n\tOrderDirection      string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Cloudreve V4\",\n\tDefaultRoot:       \"cloudreve://my\",\n\tCheckStatus:       true,\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &CloudreveV4{}\n\t})\n}\n"
  },
  {
    "path": "drivers/cloudreve_v4/types.go",
    "content": "package cloudreve_v4\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nconst (\n\tMetadataUploadSessionID = \"sys:upload_session_id\"\n\tMetadataThumbDisabled   = \"thumb:disabled\"\n)\n\ntype Object struct {\n\tmodel.Object\n\tStoragePolicy StoragePolicy\n}\n\ntype Resp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\tData any    `json:\"data\"`\n}\n\ntype BasicConfigResp struct {\n\tInstanceID string `json:\"instance_id\"`\n\t// Title        string `json:\"title\"`\n\t// Themes       string `json:\"themes\"`\n\t// DefaultTheme string `json:\"default_theme\"`\n\tUser struct {\n\t\tID string `json:\"id\"`\n\t\t// Nickname  string    `json:\"nickname\"`\n\t\t// CreatedAt time.Time `json:\"created_at\"`\n\t\t// Anonymous bool      `json:\"anonymous\"`\n\t\tGroup struct {\n\t\t\tID         string `json:\"id\"`\n\t\t\tName       string `json:\"name\"`\n\t\t\tPermission string `json:\"permission\"`\n\t\t} `json:\"group\"`\n\t} `json:\"user\"`\n\t// Logo                string `json:\"logo\"`\n\t// LogoLight           string `json:\"logo_light\"`\n\t// CaptchaReCaptchaKey string `json:\"captcha_ReCaptchaKey\"`\n\tCaptchaType string `json:\"captcha_type\"` // support 'normal' only\n\t// AppPromotion        bool   `json:\"app_promotion\"`\n}\n\ntype SiteLoginConfigResp struct {\n\tLoginCaptcha bool `json:\"login_captcha\"`\n\t// RegCaptcha bool `json:\"reg_captcha\"`\n\t// ForgetCaptcha bool `json:\"forget_captcha\"`\n\t// RegisterEnabled bool `json:\"register_enabled\"`\n\t// TosURL string `json:\"tos_url\"`\n\t// PrivacyPolicyURL string `json:\"privacy_policy_url\"`\n\t// SsoDisplayName string `json:\"sso_display_name\"`\n\t// OidcDisplayName string `json:\"oidc_display_name\"`\n}\n\ntype PrepareLoginResp struct {\n\tWebauthnEnabled bool `json:\"webauthn_enabled\"`\n\tPasswordEnabled bool `json:\"password_enabled\"`\n}\n\ntype CaptchaResp struct {\n\tImage  string `json:\"image\"`\n\tTicket string `json:\"ticket\"`\n}\n\ntype AccessJWT struct {\n\tTokenType string `json:\"token_type\"`\n\tSub       string `json:\"sub\"`\n\tExp       int64  `json:\"exp\"`\n\tNbf       int64  `json:\"nbf\"`\n}\n\ntype RefreshJWT struct {\n\tTokenType   string `json:\"token_type\"`\n\tSub         string `json:\"sub\"`\n\tExp         int    `json:\"exp\"`\n\tNbf         int    `json:\"nbf\"`\n\tStateHash   string `json:\"state_hash\"`\n\tRootTokenID string `json:\"root_token_id\"`\n}\n\ntype Token struct {\n\tAccessToken    string `json:\"access_token\"`\n\tRefreshToken   string `json:\"refresh_token\"`\n\tAccessExpires  string `json:\"access_expires\"`\n\tRefreshExpires string `json:\"refresh_expires\"`\n}\n\ntype TokenResponse struct {\n\tUser struct {\n\t\tID string `json:\"id\"`\n\t\t// Email     string    `json:\"email\"`\n\t\t// Nickname  string    `json:\"nickname\"`\n\t\tStatus string `json:\"status\"`\n\t\t// CreatedAt time.Time `json:\"created_at\"`\n\t\tGroup struct {\n\t\t\tID         string `json:\"id\"`\n\t\t\tName       string `json:\"name\"`\n\t\t\tPermission string `json:\"permission\"`\n\t\t\t// DirectLinkBatchSize int    `json:\"direct_link_batch_size\"`\n\t\t\t// TrashRetention      int    `json:\"trash_retention\"`\n\t\t} `json:\"group\"`\n\t\t// Language string `json:\"language\"`\n\t} `json:\"user\"`\n\tToken Token `json:\"token\"`\n}\n\ntype File struct {\n\tType          int            `json:\"type\"` // 0: file, 1: folder\n\tID            string         `json:\"id\"`\n\tName          string         `json:\"name\"`\n\tCreatedAt     time.Time      `json:\"created_at\"`\n\tUpdatedAt     time.Time      `json:\"updated_at\"`\n\tSize          int64          `json:\"size\"`\n\tMetadata      map[string]any `json:\"metadata,omitempty\"`\n\tPath          string         `json:\"path\"`\n\tCapability    string         `json:\"capability\"`\n\tOwned         bool           `json:\"owned\"`\n\tPrimaryEntity string         `json:\"primary_entity\"`\n}\n\nfunc fileToObject(f *File) *model.Object {\n\treturn &model.Object{\n\t\tID:       f.ID,\n\t\tPath:     f.Path,\n\t\tName:     f.Name,\n\t\tSize:     f.Size,\n\t\tModified: f.UpdatedAt,\n\t\tCtime:    f.CreatedAt,\n\t\tIsFolder: f.Type == 1,\n\t}\n}\n\ntype StoragePolicy struct {\n\tID      string `json:\"id\"`\n\tName    string `json:\"name\"`\n\tType    string `json:\"type\"`\n\tMaxSize int64  `json:\"max_size\"`\n\tRelay   bool   `json:\"relay,omitempty\"`\n}\n\ntype Pagination struct {\n\tPage      int    `json:\"page\"`\n\tPageSize  int    `json:\"page_size\"`\n\tIsCursor  bool   `json:\"is_cursor\"`\n\tNextToken string `json:\"next_token,omitempty\"`\n}\n\ntype Props struct {\n\tCapability            string   `json:\"capability\"`\n\tMaxPageSize           int      `json:\"max_page_size\"`\n\tOrderByOptions        []string `json:\"order_by_options\"`\n\tOrderDirectionOptions []string `json:\"order_direction_options\"`\n}\n\ntype FileResp struct {\n\tFiles         []File        `json:\"files\"`\n\tParent        File          `json:\"parent\"`\n\tPagination    Pagination    `json:\"pagination\"`\n\tProps         Props         `json:\"props\"`\n\tContextHint   string        `json:\"context_hint\"`\n\tMixedType     bool          `json:\"mixed_type\"`\n\tStoragePolicy StoragePolicy `json:\"storage_policy\"`\n}\n\ntype FileUrlResp struct {\n\tUrls []struct {\n\t\tURL string `json:\"url\"`\n\t} `json:\"urls\"`\n\tExpires time.Time `json:\"expires\"`\n}\n\ntype FileUploadResp struct {\n\t// UploadID       string        `json:\"upload_id\"`\n\tSessionID      string        `json:\"session_id\"`\n\tChunkSize      int64         `json:\"chunk_size\"`\n\tExpires        int64         `json:\"expires\"`\n\tStoragePolicy  StoragePolicy `json:\"storage_policy\"`\n\tURI            string        `json:\"uri\"`\n\tCompleteURL    string        `json:\"completeURL,omitempty\"`     // for S3-like\n\tCallbackSecret string        `json:\"callback_secret,omitempty\"` // for S3-like, OneDrive\n\tUploadUrls     []string      `json:\"upload_urls,omitempty\"`     // for not-local\n\tCredential     string        `json:\"credential,omitempty\"`      // for local\n}\n\ntype FileDeleteResp struct {\n\tResp\n\tData []struct {\n\t\tPath  string `json:\"path\"`\n\t\tToken string `json:\"token\"`\n\t\t// Owner struct {\n\t\t// \tOwner       string `json:\"owner\"`\n\t\t// \tApplication struct {\n\t\t// \t\tType string `json:\"type\"`\n\t\t// \t} `json:\"application\"`\n\t\t// } `json:\"owner\"`\n\t\tType int `json:\"type\"`\n\t} `json:\"data,omitempty\"`\n}\n\ntype FileThumbResp struct {\n\tURL     string    `json:\"url\"`\n\tExpires time.Time `json:\"expires\"`\n}\n\ntype FolderSummaryResp struct {\n\tFile\n\tFolderSummary struct {\n\t\tSize         int64     `json:\"size\"`\n\t\tFiles        int64     `json:\"files\"`\n\t\tFolders      int64     `json:\"folders\"`\n\t\tCompleted    bool      `json:\"completed\"`\n\t\tCalculatedAt time.Time `json:\"calculated_at\"`\n\t} `json:\"folder_summary\"`\n}\n\ntype CapacityResp struct {\n\tTotal int64 `json:\"total\"`\n\tUsed  int64 `json:\"used\"`\n\t// StoragePackTotal uint64 `json:\"storage_pack_total\"`\n}\n"
  },
  {
    "path": "drivers/cloudreve_v4/util.go",
    "content": "package cloudreve_v4\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n// do others that not defined in Driver interface\n\nconst (\n\tCodeLoginRequired     = http.StatusUnauthorized\n\tCodePathNotExist      = 40016 // Path not exist\n\tCodeCredentialInvalid = 40020 // Failed to issue token\n)\n\nvar (\n\tErrorIssueToken = errors.New(\"failed to issue token\")\n)\n\nfunc (d *CloudreveV4) getUA() string {\n\tif d.CustomUA != \"\" {\n\t\treturn d.CustomUA\n\t}\n\treturn base.UserAgent\n}\n\nfunc (d *CloudreveV4) request(method string, path string, callback base.ReqCallback, out any) error {\n\tif d.ref != nil {\n\t\treturn d.ref.request(method, path, callback, out)\n\t}\n\n\t// ensure token\n\tif d.isTokenExpired() {\n\t\terr := d.refreshToken()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn d._request(method, path, callback, out)\n}\n\nfunc (d *CloudreveV4) _request(method string, path string, callback base.ReqCallback, out any) error {\n\tif d.ref != nil {\n\t\treturn d.ref._request(method, path, callback, out)\n\t}\n\n\tu := d.Address + \"/api/v4\" + path\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":     \"application/json, text/plain, */*\",\n\t\t\"User-Agent\": d.getUA(),\n\t})\n\tif d.AccessToken != \"\" {\n\t\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\t}\n\n\tvar r Resp\n\treq.SetResult(&r)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tresp, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !resp.IsSuccess() {\n\t\treturn errors.New(resp.String())\n\t}\n\n\tif r.Code != 0 {\n\t\tif r.Code == CodeLoginRequired && d.canLogin() && path != \"/session/token/refresh\" {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn d.request(method, path, callback, out)\n\t\t}\n\t\tif r.Code == CodeCredentialInvalid {\n\t\t\treturn ErrorIssueToken\n\t\t}\n\t\tif r.Code == CodePathNotExist {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\t\treturn fmt.Errorf(\"%d: %s\", r.Code, r.Msg)\n\t}\n\n\tif out != nil && r.Data != nil {\n\t\tvar marshal []byte\n\t\tmarshal, err = json.Marshal(r.Data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = json.Unmarshal(marshal, out)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *CloudreveV4) canLogin() bool {\n\treturn d.Username != \"\" && d.Password != \"\"\n}\n\nfunc (d *CloudreveV4) login() error {\n\tvar siteConfig SiteLoginConfigResp\n\terr := d._request(http.MethodGet, \"/site/config/login\", nil, &siteConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar prepareLogin PrepareLoginResp\n\terr = d._request(http.MethodGet, \"/session/prepare?email=\"+d.Addition.Username, nil, &prepareLogin)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !prepareLogin.PasswordEnabled {\n\t\treturn errors.New(\"password not enabled\")\n\t}\n\tif prepareLogin.WebauthnEnabled {\n\t\treturn errors.New(\"webauthn not support\")\n\t}\n\tfor range 5 {\n\t\terr = d.doLogin(siteConfig.LoginCaptcha)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif err.Error() != \"CAPTCHA not match.\" {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *CloudreveV4) doLogin(needCaptcha bool) error {\n\tvar err error\n\tloginBody := base.Json{\n\t\t\"email\":    d.Username,\n\t\t\"password\": d.Password,\n\t}\n\tif needCaptcha {\n\t\tvar config BasicConfigResp\n\t\terr = d._request(http.MethodGet, \"/site/config/basic\", nil, &config)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif config.CaptchaType != \"normal\" {\n\t\t\treturn fmt.Errorf(\"captcha type %s not support\", config.CaptchaType)\n\t\t}\n\t\tvar captcha CaptchaResp\n\t\terr = d._request(http.MethodGet, \"/site/captcha\", nil, &captcha)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !strings.HasPrefix(captcha.Image, \"data:image/png;base64,\") {\n\t\t\treturn errors.New(\"can not get captcha\")\n\t\t}\n\t\tloginBody[\"ticket\"] = captcha.Ticket\n\t\ti := strings.Index(captcha.Image, \",\")\n\t\tdec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha.Image[i+1:]))\n\t\tvRes, err := base.RestyClient.R().SetMultipartField(\n\t\t\t\"image\", \"validateCode.png\", \"image/png\", dec).\n\t\t\tPost(setting.GetStr(conf.OcrApi))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif jsoniter.Get(vRes.Body(), \"status\").ToInt() != 200 {\n\t\t\treturn errors.New(\"ocr error:\" + jsoniter.Get(vRes.Body(), \"msg\").ToString())\n\t\t}\n\t\tcaptchaCode := jsoniter.Get(vRes.Body(), \"result\").ToString()\n\t\tif captchaCode == \"\" {\n\t\t\treturn errors.New(\"ocr error: empty result\")\n\t\t}\n\t\tloginBody[\"captcha\"] = captchaCode\n\t}\n\tvar token TokenResponse\n\terr = d._request(http.MethodPost, \"/session/token\", func(req *resty.Request) {\n\t\treq.SetBody(loginBody)\n\t}, &token)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken\n\td.AccessExpires, d.RefreshExpires = token.Token.AccessExpires, token.Token.RefreshExpires\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *CloudreveV4) refreshToken() error {\n\t// if no refresh token, try to login if possible\n\tif d.RefreshToken == \"\" {\n\t\tif d.canLogin() {\n\t\t\terr := d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot login to get refresh token, error: %s\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// parse jwt to check if refresh token is valid\n\tvar jwt RefreshJWT\n\terr := d.parseJWT(d.RefreshToken, &jwt)\n\tif err != nil {\n\t\t// if refresh token is invalid, try to login if possible\n\t\tif d.canLogin() {\n\t\t\treturn d.login()\n\t\t}\n\t\td.GetStorage().SetStatus(fmt.Sprintf(\"Invalid RefreshToken: %s\", err.Error()))\n\t\top.MustSaveDriverStorage(d)\n\t\treturn fmt.Errorf(\"invalid refresh token: %w\", err)\n\t}\n\n\t// do refresh token\n\tvar token Token\n\terr = d._request(http.MethodPost, \"/session/token/refresh\", func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t})\n\t}, &token)\n\tif err != nil {\n\t\tif errors.Is(err, ErrorIssueToken) {\n\t\t\tif d.canLogin() {\n\t\t\t\t// try to login again\n\t\t\t\treturn d.login()\n\t\t\t}\n\t\t\td.GetStorage().SetStatus(\"This session is no longer valid\")\n\t\t\top.MustSaveDriverStorage(d)\n\t\t\treturn ErrorIssueToken\n\t\t}\n\t\treturn err\n\t}\n\td.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken\n\td.AccessExpires, d.RefreshExpires = token.AccessExpires, token.RefreshExpires\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *CloudreveV4) parseJWT(token string, jwt any) error {\n\tsplit := strings.Split(token, \".\")\n\tif len(split) != 3 {\n\t\treturn fmt.Errorf(\"invalid token length: %d, ensure the token is a valid JWT\", len(split))\n\t}\n\tdata, err := base64.RawURLEncoding.DecodeString(split[1])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid token encoding: %w, ensure the token is a valid JWT\", err)\n\t}\n\terr = json.Unmarshal(data, &jwt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid token content: %w, ensure the token is a valid JWT\", err)\n\t}\n\treturn nil\n}\n\n// check if token is expired\n// https://github.com/cloudreve/frontend/blob/ddfacc1c31c49be03beb71de4cc114c8811038d6/src/session/index.ts#L177-L200\nfunc (d *CloudreveV4) isTokenExpired() bool {\n\tif d.RefreshToken == \"\" {\n\t\t// login again if username and password is set\n\t\tif d.canLogin() {\n\t\t\treturn true\n\t\t}\n\t\t// no refresh token, cannot refresh\n\t\treturn false\n\t}\n\tif d.AccessToken == \"\" {\n\t\treturn true\n\t}\n\tvar (\n\t\terr     error\n\t\texpires time.Time\n\t)\n\t// check if token is expired\n\tif d.AccessExpires != \"\" {\n\t\t// use expires field if possible to prevent timezone issue\n\t\t// only available after login or refresh token\n\t\t// 2025-08-28T02:43:07.645109985+08:00\n\t\texpires, err = time.Parse(time.RFC3339Nano, d.AccessExpires)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t} else {\n\t\t// fallback to parse jwt\n\t\t// if failed, disable the storage\n\t\tvar jwt AccessJWT\n\t\terr = d.parseJWT(d.AccessToken, &jwt)\n\t\tif err != nil {\n\t\t\td.GetStorage().SetStatus(fmt.Sprintf(\"Invalid AccessToken: %s\", err.Error()))\n\t\t\top.MustSaveDriverStorage(d)\n\t\t\treturn false\n\t\t}\n\t\t// may be have timezone issue\n\t\texpires = time.Unix(jwt.Exp, 0)\n\t}\n\t// add a 10 minutes safe margin\n\tddl := time.Now().Add(10 * time.Minute)\n\tif expires.Before(ddl) {\n\t\t// current access token expired, check if refresh token is expired\n\t\t// warning: cannot parse refresh token from jwt, because the exp field is not standard\n\t\tif d.RefreshExpires != \"\" {\n\t\t\trefreshExpires, err := time.Parse(time.RFC3339Nano, d.RefreshExpires)\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif refreshExpires.Before(time.Now()) {\n\t\t\t\t// This session is no longer valid\n\t\t\t\tif d.canLogin() {\n\t\t\t\t\t// try to login again\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\td.GetStorage().SetStatus(\"This session is no longer valid\")\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tif DEFAULT == 0 {\n\t\t// support relay\n\t\tDEFAULT = file.GetSize()\n\t}\n\tfor finish < file.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := file.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[CloudreveV4-Local] upload range: %d-%d/%d\", finish, finish+byteSize-1, file.GetSize())\n\t\tbyteData := make([]byte, byteSize)\n\t\tn, err := io.ReadFull(file, byteData)\n\t\tutils.Log.Debug(err, n)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = d.request(http.MethodPost, \"/file/upload/\"+u.SessionID+\"/\"+strconv.Itoa(chunk), func(req *resty.Request) {\n\t\t\treq.SetHeader(\"Content-Type\", \"application/octet-stream\")\n\t\t\treq.SetContentLength(true)\n\t\t\treq.SetHeader(\"Content-Length\", strconv.FormatInt(byteSize, 10))\n\t\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\t\treq.AddRetryCondition(func(r *resty.Response, err error) bool {\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif r.IsError() {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tvar retryResp Resp\n\t\t\t\tjErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)\n\t\t\t\tif jErr != nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif retryResp.Code != 0 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\tchunk++\n\t}\n\treturn nil\n}\n\nfunc (d *CloudreveV4) upRemote(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := stream.NewStreamSectionReader(file, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := u.UploadUrls[0]\n\tcredential := u.Credential\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tfor finish < file.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := file.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[CloudreveV4-Remote] upload range: %d-%d/%d\", finish, finish+byteSize-1, file.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl+\"?chunk=\"+strconv.Itoa(chunk),\n\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Authorization\", fmt.Sprint(credential))\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\tif res.StatusCode != 200 {\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\t}\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tvar up Resp\n\t\t\t\terr = json.Unmarshal(body, &up)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif up.Code != 0 {\n\t\t\t\t\treturn errors.New(up.Msg)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\tchunk++\n\t}\n\treturn nil\n}\n\nfunc (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := stream.NewStreamSectionReader(file, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := u.UploadUrls[0]\n\tvar finish int64 = 0\n\tfor finish < file.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := file.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[CloudreveV4-OneDrive] upload range: %d-%d/%d\", finish, finish+byteSize-1, file.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, file.GetSize()))\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\t\t\treturn errors.New(string(data))\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t}\n\t// 上传成功发送回调请求\n\treturn d.request(http.MethodPost, \"/callback/onedrive/\"+u.SessionID+\"/\"+u.CallbackSecret, func(req *resty.Request) {\n\t\treq.SetBody(\"{}\")\n\t}, nil)\n}\n\nfunc (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress, s3Type string) error {\n\tDEFAULT := int64(u.ChunkSize)\n\tss, err := stream.NewStreamSectionReader(file, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tvar etags []string\n\tfor finish < file.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := file.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[CloudreveV4-S3] upload range: %d-%d/%d\", finish, finish+byteSize-1, file.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, u.UploadUrls[chunk],\n\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\t\t\tif s3Type == \"ks3\" {\n\t\t\t\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t\t\t\t}\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tetag := res.Header.Get(\"ETag\")\n\t\t\t\tres.Body.Close()\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode != 200:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase etag == \"\":\n\t\t\t\t\treturn errors.New(\"failed to get ETag from header\")\n\t\t\t\tdefault:\n\t\t\t\t\tetags = append(etags, etag)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\tchunk++\n\t}\n\n\t// s3LikeFinishUpload\n\tbodyBuilder := &strings.Builder{}\n\tbodyBuilder.WriteString(\"<CompleteMultipartUpload>\")\n\tfor i, etag := range etags {\n\t\tbodyBuilder.WriteString(fmt.Sprintf(\n\t\t\t`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,\n\t\t\ti+1, // PartNumber 从 1 开始\n\t\t\tetag,\n\t\t))\n\t}\n\tbodyBuilder.WriteString(\"</CompleteMultipartUpload>\")\n\treq, err := http.NewRequestWithContext(ctx,\n\t\thttp.MethodPost,\n\t\tu.CompleteURL,\n\t\tstrings.NewReader(bodyBuilder.String()),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif s3Type == \"ks3\" {\n\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t} else {\n\t\treq.Header.Set(\"Content-Type\", \"application/xml\")\n\t}\n\treq.Header.Set(\"User-Agent\", d.getUA())\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(res.Body)\n\t\treturn fmt.Errorf(\"up status: %d, error: %s\", res.StatusCode, string(body))\n\t}\n\n\t// 上传成功发送回调请求\n\treturn d.request(http.MethodGet, \"/callback/\"+s3Type+\"/\"+u.SessionID+\"/\"+u.CallbackSecret, nil, nil)\n}\n"
  },
  {
    "path": "drivers/cnb_releases/driver.go",
    "content": "package cnb_releases\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype CnbReleases struct {\n\tmodel.Storage\n\tAddition\n\tref *CnbReleases\n}\n\nfunc (d *CnbReleases) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *CnbReleases) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *CnbReleases) Init(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *CnbReleases) InitReference(storage driver.Driver) error {\n\trefStorage, ok := storage.(*CnbReleases)\n\tif ok {\n\t\td.ref = refStorage\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"ref: storage is not CnbReleases\")\n}\n\nfunc (d *CnbReleases) Drop(ctx context.Context) error {\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *CnbReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tdirID := dir.GetID()\n\tif dirID == \"\" {\n\t\t// get all releases for root dir\n\t\tvar resp ReleaseList\n\n\t\terr := d.Request(http.MethodGet, \"/{repo}/-/releases\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn utils.SliceConvert(resp, func(src Release) (model.Obj, error) {\n\t\t\tname := src.Name\n\t\t\tif d.UseTagName {\n\t\t\t\tname = src.TagName\n\t\t\t}\n\t\t\treturn &model.Object{\n\t\t\t\tID:       src.ID,\n\t\t\t\tName:     name,\n\t\t\t\tSize:     d.sumAssetsSize(src.Assets),\n\t\t\t\tCtime:    src.CreatedAt,\n\t\t\t\tModified: src.UpdatedAt,\n\t\t\t\tIsFolder: true,\n\t\t\t}, nil\n\t\t})\n\t}\n\n\tvar resp Release\n\terr := d.Request(http.MethodGet, \"/{repo}/-/releases/{release_id}\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\treq.SetPathParam(\"release_id\", dirID)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.SliceConvert(resp.Assets, func(src ReleaseAsset) (model.Obj, error) {\n\t\treturn &Object{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       src.ID,\n\t\t\t\tPath:     src.Path,\n\t\t\t\tName:     src.Name,\n\t\t\t\tSize:     src.Size,\n\t\t\t\tCtime:    src.CreatedAt,\n\t\t\t\tModified: src.UpdatedAt,\n\t\t\t\tIsFolder: false,\n\t\t\t},\n\t\t\tParentID: dirID,\n\t\t}, nil\n\t})\n\n}\n\nfunc (d *CnbReleases) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\treturn &model.Link{\n\t\tURL: \"https://cnb.cool\" + file.GetPath(),\n\t}, nil\n}\n\nfunc (d *CnbReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif parentDir.GetPath() == \"/\" {\n\t\t// create a new release\n\t\tbranch := d.DefaultBranch\n\t\tif branch == \"\" {\n\t\t\tbranch = \"main\" // fallback to \"main\" if not set\n\t\t}\n\t\treturn d.Request(http.MethodPost, \"/{repo}/-/releases\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"name\":             dirName,\n\t\t\t\t\"tag_name\":         dirName,\n\t\t\t\t\"target_commitish\": branch,\n\t\t\t})\n\t\t}, nil)\n\t}\n\treturn errs.NotImplement\n}\n\nfunc (d *CnbReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CnbReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif srcObj.IsDir() && !d.UseTagName {\n\t\treturn d.Request(http.MethodPatch, \"/{repo}/-/releases/{release_id}\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\t\treq.SetPathParam(\"release_id\", srcObj.GetID())\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"name\": newName,\n\t\t\t})\n\t\t}, nil)\n\t}\n\treturn errs.NotImplement\n}\n\nfunc (d *CnbReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *CnbReleases) Remove(ctx context.Context, obj model.Obj) error {\n\tif obj.IsDir() {\n\t\treturn d.Request(http.MethodDelete, \"/{repo}/-/releases/{release_id}\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\t\treq.SetPathParam(\"release_id\", obj.GetID())\n\t\t}, nil)\n\t}\n\tif o, ok := obj.(*Object); ok {\n\t\treturn d.Request(http.MethodDelete, \"/{repo}/-/releases/{release_id}/assets/{asset_id}\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\t\treq.SetPathParam(\"release_id\", o.ParentID)\n\t\t\treq.SetPathParam(\"asset_id\", obj.GetID())\n\t\t}, nil)\n\t} else {\n\t\treturn fmt.Errorf(\"unable to get release ID\")\n\t}\n}\n\nfunc (d *CnbReleases) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\t// 1. get upload info\n\tvar resp ReleaseAssetUploadURL\n\terr := d.Request(http.MethodPost, \"/{repo}/-/releases/{release_id}/asset-upload-url\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"repo\", d.Repo)\n\t\treq.SetPathParam(\"release_id\", dstDir.GetID())\n\t\treq.SetBody(base.Json{\n\t\t\t\"asset_name\": file.GetName(),\n\t\t\t\"overwrite\":  true,\n\t\t\t\"size\":       file.GetSize(),\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 2. upload file\n\t// use multipart to create form file\n\tvar b bytes.Buffer\n\tw := multipart.NewWriter(&b)\n\t_, err = w.CreateFormFile(\"file\", file.GetName())\n\tif err != nil {\n\t\treturn err\n\t}\n\theadSize := b.Len()\n\terr = w.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\thead := bytes.NewReader(b.Bytes()[:headSize])\n\ttail := bytes.NewReader(b.Bytes()[headSize:])\n\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\tReader: io.MultiReader(head, file, tail),\n\t\t\tSize:   int64(b.Len()) + file.GetSize(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\n\t// use net/http to upload file\n\tctxWithTimeout, cancel := context.WithTimeout(ctx, time.Duration(resp.ExpiresInSec+1)*time.Second)\n\tdefer cancel()\n\treq, err := http.NewRequestWithContext(ctxWithTimeout, http.MethodPost, resp.UploadURL, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", w.FormDataContentType())\n\treq.Header.Set(\"User-Agent\", base.UserAgent)\n\thttpResp, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer httpResp.Body.Close()\n\tif httpResp.StatusCode != http.StatusNoContent {\n\t\treturn fmt.Errorf(\"upload file failed: %s\", httpResp.Status)\n\t}\n\n\t// 3. verify upload\n\treturn d.Request(http.MethodPost, resp.VerifyURL, nil, nil)\n}\n\nvar _ driver.Driver = (*CnbReleases)(nil)\n"
  },
  {
    "path": "drivers/cnb_releases/meta.go",
    "content": "package cnb_releases\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRepo          string `json:\"repo\" type:\"string\" required:\"true\"`\n\tToken         string `json:\"token\" type:\"string\" required:\"true\"`\n\tUseTagName    bool   `json:\"use_tag_name\" type:\"bool\" default:\"false\" help:\"Use tag name instead of release name\"`\n\tDefaultBranch string `json:\"default_branch\" type:\"string\" default:\"main\" help:\"Default branch for new releases\"`\n}\n\nvar config = driver.Config{\n\tName:      \"CNB Releases\",\n\tLocalSort: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &CnbReleases{}\n\t})\n}\n"
  },
  {
    "path": "drivers/cnb_releases/types.go",
    "content": "package cnb_releases\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Object struct {\n\tmodel.Object\n\tParentID string\n}\n\ntype TagList []Tag\n\ntype Tag struct {\n\tCommit struct {\n\t\tAuthor    UserInfo       `json:\"author\"`\n\t\tCommit    CommitObject   `json:\"commit\"`\n\t\tCommitter UserInfo       `json:\"committer\"`\n\t\tParents   []CommitParent `json:\"parents\"`\n\t\tSha       string         `json:\"sha\"`\n\t} `json:\"commit\"`\n\tName         string                `json:\"name\"`\n\tTarget       string                `json:\"target\"`\n\tTargetType   string                `json:\"target_type\"`\n\tVerification TagObjectVerification `json:\"verification\"`\n}\n\ntype UserInfo struct {\n\tFreeze   bool   `json:\"freeze\"`\n\tNickname string `json:\"nickname\"`\n\tUsername string `json:\"username\"`\n}\n\ntype CommitObject struct {\n\tAuthor       Signature                `json:\"author\"`\n\tCommentCount int                      `json:\"comment_count\"`\n\tCommitter    Signature                `json:\"committer\"`\n\tMessage      string                   `json:\"message\"`\n\tTree         CommitObjectTree         `json:\"tree\"`\n\tVerification CommitObjectVerification `json:\"verification\"`\n}\n\ntype Signature struct {\n\tDate  time.Time `json:\"date\"`\n\tEmail string    `json:\"email\"`\n\tName  string    `json:\"name\"`\n}\n\ntype CommitObjectTree struct {\n\tSha string `json:\"sha\"`\n}\n\ntype CommitObjectVerification struct {\n\tPayload    string `json:\"payload\"`\n\tReason     string `json:\"reason\"`\n\tSignature  string `json:\"signature\"`\n\tVerified   bool   `json:\"verified\"`\n\tVerifiedAt string `json:\"verified_at\"`\n}\n\ntype CommitParent = CommitObjectTree\n\ntype TagObjectVerification = CommitObjectVerification\n\ntype ReleaseList []Release\n\ntype Release struct {\n\tAssets       []ReleaseAsset `json:\"assets\"`\n\tAuthor       UserInfo       `json:\"author\"`\n\tBody         string         `json:\"body\"`\n\tCreatedAt    time.Time      `json:\"created_at\"`\n\tDraft        bool           `json:\"draft\"`\n\tID           string         `json:\"id\"`\n\tIsLatest     bool           `json:\"is_latest\"`\n\tName         string         `json:\"name\"`\n\tPrerelease   bool           `json:\"prerelease\"`\n\tPublishedAt  time.Time      `json:\"published_at\"`\n\tTagCommitish string         `json:\"tag_commitish\"`\n\tTagName      string         `json:\"tag_name\"`\n\tUpdatedAt    time.Time      `json:\"updated_at\"`\n}\n\ntype ReleaseAsset struct {\n\tContentType string    `json:\"content_type\"`\n\tCreatedAt   time.Time `json:\"created_at\"`\n\tID          string    `json:\"id\"`\n\tName        string    `json:\"name\"`\n\tPath        string    `json:\"path\"`\n\tSize        int64     `json:\"size\"`\n\tUpdatedAt   time.Time `json:\"updated_at\"`\n\tUploader    UserInfo  `json:\"uploader\"`\n}\n\ntype ReleaseAssetUploadURL struct {\n\tUploadURL    string `json:\"upload_url\"`\n\tExpiresInSec int    `json:\"expires_in_sec\"`\n\tVerifyURL    string `json:\"verify_url\"`\n}\n"
  },
  {
    "path": "drivers/cnb_releases/util.go",
    "content": "package cnb_releases\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *CnbReleases) Request(method string, path string, callback base.ReqCallback, resp any) error {\n\tif d.ref != nil {\n\t\treturn d.ref.Request(method, path, callback, resp)\n\t}\n\tvar url string\n\tif strings.HasPrefix(path, \"http\") {\n\t\turl = path\n\t} else {\n\t\turl = \"https://api.cnb.cool\" + path\n\t}\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Accept\", \"application/json\")\n\treq.SetAuthScheme(\"Bearer\")\n\treq.SetAuthToken(d.Token)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tres, err := req.Execute(method, url)\n\tlog.Debugln(res.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() != http.StatusOK && res.StatusCode() != http.StatusCreated && res.StatusCode() != http.StatusNoContent {\n\t\treturn fmt.Errorf(\"failed to request %s, status code: %d, message: %s\", url, res.StatusCode(), res.String())\n\t}\n\n\tif resp != nil {\n\t\terr = json.Unmarshal(res.Body(), resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *CnbReleases) sumAssetsSize(assets []ReleaseAsset) int64 {\n\tvar size int64\n\tfor _, asset := range assets {\n\t\tsize += asset.Size\n\t}\n\treturn size\n}\n"
  },
  {
    "path": "drivers/crypt/driver.go",
    "content": "package crypt\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\trcCrypt \"github.com/rclone/rclone/backend/crypt\"\n\t\"github.com/rclone/rclone/fs/config/configmap\"\n\t\"github.com/rclone/rclone/fs/config/obscure\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Crypt struct {\n\tmodel.Storage\n\tAddition\n\tcipher *rcCrypt.Cipher\n}\n\nconst obfuscatedPrefix = \"___Obfuscated___\"\n\nfunc (d *Crypt) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Crypt) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Crypt) Init(ctx context.Context) error {\n\t// obfuscate credentials if it's updated or just created\n\terr := d.updateObfusParm(&d.Password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to obfuscate password: %w\", err)\n\t}\n\terr = d.updateObfusParm(&d.Salt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to obfuscate salt: %w\", err)\n\t}\n\n\tisCryptExt := regexp.MustCompile(`^[.][A-Za-z0-9-_]{2,}$`).MatchString\n\tif !isCryptExt(d.EncryptedSuffix) {\n\t\treturn fmt.Errorf(\"EncryptedSuffix is Illegal\")\n\t}\n\td.FileNameEncoding = utils.GetNoneEmpty(d.FileNameEncoding, \"base64\")\n\td.EncryptedSuffix = utils.GetNoneEmpty(d.EncryptedSuffix, \".bin\")\n\td.RemotePath = utils.FixAndCleanPath(d.RemotePath)\n\n\tp, _ := strings.CutPrefix(d.Password, obfuscatedPrefix)\n\tp2, _ := strings.CutPrefix(d.Salt, obfuscatedPrefix)\n\tconfig := configmap.Simple{\n\t\t\"password\":                  p,\n\t\t\"password2\":                 p2,\n\t\t\"filename_encryption\":       d.FileNameEnc,\n\t\t\"directory_name_encryption\": d.DirNameEnc,\n\t\t\"filename_encoding\":         d.FileNameEncoding,\n\t\t\"suffix\":                    d.EncryptedSuffix,\n\t\t\"pass_bad_blocks\":           \"\",\n\t}\n\tc, err := rcCrypt.NewCipher(config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Cipher: %w\", err)\n\t}\n\td.cipher = c\n\n\treturn nil\n}\n\nfunc (d *Crypt) updateObfusParm(str *string) error {\n\ttemp := *str\n\tif !strings.HasPrefix(temp, obfuscatedPrefix) {\n\t\ttemp, err := obscure.Obscure(temp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttemp = obfuscatedPrefix + temp\n\t\t*str = temp\n\t}\n\treturn nil\n}\n\nfunc (d *Crypt) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tremoteFullPath := dir.GetPath()\n\tobjs, err := fs.List(ctx, remoteFullPath, &fs.ListArgs{NoLog: true, Refresh: args.Refresh})\n\t// the obj must implement the model.SetPath interface\n\t// return objs, err\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make([]model.Obj, 0, len(objs))\n\tfor _, obj := range objs {\n\t\tsize := obj.GetSize()\n\t\tmask := model.GetObjMask(obj)\n\t\tname := obj.GetName()\n\t\tif mask&model.Virtual == 0 {\n\t\t\tif obj.IsDir() {\n\t\t\t\tname, err = d.cipher.DecryptDirName(model.UnwrapObjName(obj).GetName())\n\t\t\t\tif err != nil {\n\t\t\t\t\t// filter illegal files\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsize, err = d.cipher.DecryptedSize(size)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// filter illegal files\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tname, err = d.cipher.DecryptFileName(model.UnwrapObjName(obj).GetName())\n\t\t\t\tif err != nil {\n\t\t\t\t\t// filter illegal files\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !d.ShowHidden && strings.HasPrefix(name, \".\") {\n\t\t\tcontinue\n\t\t}\n\t\tobjRes := &model.Object{\n\t\t\tPath:     stdpath.Join(remoteFullPath, obj.GetName()),\n\t\t\tName:     name,\n\t\t\tSize:     size,\n\t\t\tModified: obj.ModTime(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t\tCtime:    obj.CreateTime(),\n\t\t\tMask:     mask &^ model.Temp,\n\t\t\t// discarding hash as it's encrypted\n\t\t}\n\t\tif !d.Thumbnail || !strings.HasPrefix(args.ReqPath, \"/\") {\n\t\t\tresult = append(result, objRes)\n\t\t\tcontinue\n\t\t}\n\t\tthumbPath := stdpath.Join(args.ReqPath, \".thumbnails\", name+\".webp\")\n\t\tthumb := fmt.Sprintf(\"%s/d%s?sign=%s\",\n\t\t\tcommon.GetApiUrl(ctx),\n\t\t\tutils.EncodePath(thumbPath, true),\n\t\t\tsign.Sign(thumbPath))\n\t\tresult = append(result, &model.ObjThumb{\n\t\t\tObject: *objRes,\n\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\tThumbnail: thumb,\n\t\t\t},\n\t\t})\n\t}\n\n\treturn result, nil\n}\n\nfunc (a Addition) GetRootPath() string {\n\treturn a.RemotePath\n}\n\nfunc (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) {\n\tfirstTryIsFolder, secondTry := guessPath(path)\n\tremoteFullPath := stdpath.Join(d.RemotePath, d.encryptPath(path, firstTryIsFolder))\n\tremoteObj, err := fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\tif errors.Is(err, errs.StorageNotFound) {\n\t\t\tremoteFullPath = stdpath.Join(d.RemotePath, path)\n\t\t\tremoteObj, err = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})\n\t\t\tif err != nil {\n\t\t\t\t// 可能是 虚拟路径+开启文件夹加密：返回NotSupport让op.Get去尝试op.List查找\n\t\t\t\treturn nil, errs.NotSupport\n\t\t\t}\n\t\t} else if secondTry && errs.IsObjectNotFound(err) {\n\t\t\t// try the opposite\n\t\t\tremoteFullPath = stdpath.Join(d.RemotePath, d.encryptPath(path, !firstTryIsFolder))\n\t\t\tremoteObj, err = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tsize := remoteObj.GetSize()\n\tname := remoteObj.GetName()\n\tmask := model.GetObjMask(remoteObj) &^ model.Temp\n\tif mask&model.Virtual == 0 {\n\t\tif !remoteObj.IsDir() {\n\t\t\tdecryptedSize, err := d.cipher.DecryptedSize(size)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"DecryptedSize failed for %s ,will use original size, err:%s\", path, err)\n\t\t\t} else {\n\t\t\t\tsize = decryptedSize\n\t\t\t}\n\t\t\tdecryptedName, err := d.cipher.DecryptFileName(model.UnwrapObjName(remoteObj).GetName())\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"DecryptFileName failed for %s ,will use original name, err:%s\", path, err)\n\t\t\t} else {\n\t\t\t\tname = decryptedName\n\t\t\t}\n\t\t} else {\n\t\t\tdecryptedName, err := d.cipher.DecryptDirName(model.UnwrapObjName(remoteObj).GetName())\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"DecryptDirName failed for %s ,will use original name, err:%s\", path, err)\n\t\t\t} else {\n\t\t\t\tname = decryptedName\n\t\t\t}\n\t\t}\n\t}\n\treturn &model.Object{\n\t\tPath:     remoteFullPath,\n\t\tName:     name,\n\t\tSize:     size,\n\t\tModified: remoteObj.ModTime(),\n\t\tIsFolder: remoteObj.IsDir(),\n\t\tCtime:    remoteObj.CreateTime(),\n\t\tMask:     mask,\n\t}, nil\n}\n\n// https://github.com/rclone/rclone/blob/v1.67.0/backend/crypt/cipher.go#L37\nconst fileHeaderSize = 32\n\nfunc (d *Crypt) Link(ctx context.Context, file model.Obj, _ model.LinkArgs) (*model.Link, error) {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tremoteLink, remoteFile, err := op.Link(ctx, remoteStorage, remoteActualPath, model.LinkArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tremoteSize := remoteLink.ContentLength\n\tif remoteSize <= 0 {\n\t\tremoteSize = remoteFile.GetSize()\n\t}\n\trrf, err := stream.GetRangeReaderFromLink(remoteSize, remoteLink)\n\tif err != nil {\n\t\t_ = remoteLink.Close()\n\t\treturn nil, fmt.Errorf(\"the remote storage driver need to be enhanced to support encrytion\")\n\t}\n\n\tmu := &sync.Mutex{}\n\tvar fileHeader []byte\n\trangeReaderFunc := func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) {\n\t\tlength := limit\n\t\tif offset == 0 && limit > 0 {\n\t\t\tmu.Lock()\n\t\t\tif limit <= fileHeaderSize {\n\t\t\t\tdefer mu.Unlock()\n\t\t\t\tif fileHeader != nil {\n\t\t\t\t\treturn io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil\n\t\t\t\t}\n\t\t\t\tlength = fileHeaderSize\n\t\t\t} else if fileHeader == nil {\n\t\t\t\tdefer mu.Unlock()\n\t\t\t} else {\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}\n\n\t\tremoteReader, err := rrf.RangeRead(ctx, http_range.Range{Start: offset, Length: length})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif offset == 0 && limit > 0 {\n\t\t\tfileHeader = make([]byte, fileHeaderSize)\n\t\t\tn, err := io.ReadFull(remoteReader, fileHeader)\n\t\t\tif n != fileHeaderSize {\n\t\t\t\tfileHeader = nil\n\t\t\t\treturn nil, fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", fileHeaderSize, n, err)\n\t\t\t}\n\t\t\tif limit <= fileHeaderSize {\n\t\t\t\tremoteReader.Close()\n\t\t\t\treturn io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil\n\t\t\t} else {\n\t\t\t\tremoteReader = utils.ReadCloser{\n\t\t\t\t\tReader: io.MultiReader(bytes.NewReader(fileHeader), remoteReader),\n\t\t\t\t\tCloser: remoteReader,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn remoteReader, nil\n\t}\n\treturn &model.Link{\n\t\tRangeReader: stream.RangeReaderFunc(func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\treadSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn readSeeker, nil\n\t\t}),\n\t\tSyncClosers:      utils.NewSyncClosers(remoteLink),\n\t\tRequireReference: remoteLink.RequireReference,\n\t}, nil\n}\n\nfunc (d *Crypt) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(parentDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tencryptedName := d.cipher.EncryptDirName(dirName)\n\treturn op.MakeDir(ctx, remoteStorage, stdpath.Join(remoteActualPath, encryptedName))\n}\n\nfunc (d *Crypt) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := fs.Move(ctx, srcObj.GetPath(), dstDir.GetPath())\n\treturn err\n}\n\nfunc (d *Crypt) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar newEncryptedName string\n\tif srcObj.IsDir() {\n\t\tnewEncryptedName = d.cipher.EncryptDirName(newName)\n\t} else {\n\t\tnewEncryptedName = d.cipher.EncryptFileName(newName)\n\t}\n\treturn op.Rename(ctx, remoteStorage, remoteActualPath, newEncryptedName)\n}\n\nfunc (d *Crypt) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := fs.Copy(ctx, srcObj.GetPath(), dstDir.GetPath())\n\treturn err\n}\n\nfunc (d *Crypt) Remove(ctx context.Context, obj model.Obj) error {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(obj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn op.Remove(ctx, remoteStorage, remoteActualPath)\n}\n\nfunc (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {\n\tremoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Encrypt the data into wrappedIn\n\twrappedIn, err := d.cipher.EncryptData(streamer)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to EncryptData: %w\", err)\n\t}\n\n\t// doesn't support seekableStream, since rapid-upload is not working for encrypted data\n\tstreamOut := &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tID:       streamer.GetID(),\n\t\t\tPath:     streamer.GetPath(),\n\t\t\tName:     d.cipher.EncryptFileName(streamer.GetName()),\n\t\t\tSize:     d.cipher.EncryptedSize(streamer.GetSize()),\n\t\t\tModified: streamer.ModTime(),\n\t\t\tIsFolder: streamer.IsDir(),\n\t\t},\n\t\tReader:            wrappedIn,\n\t\tMimetype:          \"application/octet-stream\",\n\t\tForceStreamUpload: true,\n\t\tExist:             streamer.GetExist(),\n\t}\n\treturn op.Put(ctx, remoteStorage, remoteActualPath, streamOut, up)\n}\n\nfunc (d *Crypt) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tremoteStorage, _, err := op.GetStorageAndActualPath(d.RemotePath)\n\tif err != nil {\n\t\treturn nil, errs.NotImplement\n\t}\n\tremoteDetails, err := op.GetStorageDetails(ctx, remoteStorage)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: remoteDetails.DiskUsage,\n\t}, nil\n}\n\nvar _ driver.Driver = (*Crypt)(nil)\n"
  },
  {
    "path": "drivers/crypt/meta.go",
    "content": "package crypt\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tFileNameEnc string `json:\"filename_encryption\" type:\"select\" required:\"true\" options:\"off,standard,obfuscate\" default:\"off\"`\n\tDirNameEnc  string `json:\"directory_name_encryption\" type:\"select\" required:\"true\" options:\"false,true\" default:\"false\"`\n\tRemotePath  string `json:\"remote_path\" required:\"true\" help:\"This is where the encrypted data stores\"`\n\n\tPassword         string `json:\"password\" required:\"true\" confidential:\"true\" help:\"the main password\"`\n\tSalt             string `json:\"salt\" confidential:\"true\"  help:\"If you don't know what is salt, treat it as a second password. Optional but recommended\"`\n\tEncryptedSuffix  string `json:\"encrypted_suffix\" required:\"true\" default:\".bin\" help:\"for advanced user only! encrypted files will have this suffix\"`\n\tFileNameEncoding string `json:\"filename_encoding\" type:\"select\" required:\"true\" options:\"base64,base32,base32768\" default:\"base64\" help:\"for advanced user only!\"`\n\n\tThumbnail bool `json:\"thumbnail\" required:\"true\" default:\"false\" help:\"enable thumbnail which pre-generated under .thumbnails folder\"`\n\n\tShowHidden bool `json:\"show_hidden\"  default:\"true\" required:\"false\" help:\"show hidden directories and files\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Crypt\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tNoCache:     true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n\tCheckStatus: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Crypt{}\n\t})\n}\n"
  },
  {
    "path": "drivers/crypt/types.go",
    "content": "package crypt\n"
  },
  {
    "path": "drivers/crypt/util.go",
    "content": "package crypt\n\nimport (\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// will give the best guessing based on the path\nfunc guessPath(path string) (isFolder, secondTry bool) {\n\tif strings.HasSuffix(path, \"/\") {\n\t\t//confirmed a folder\n\t\treturn true, false\n\t}\n\tlastSlash := strings.LastIndex(path, \"/\")\n\tif !strings.Contains(path[lastSlash:], \".\") {\n\t\t//no dot, try folder then try file\n\t\treturn true, true\n\t}\n\treturn false, true\n}\n\nfunc (d *Crypt) encryptPath(path string, isFolder bool) string {\n\tif isFolder {\n\t\treturn d.cipher.EncryptDirName(path)\n\t}\n\tdir, fileName := filepath.Split(path)\n\treturn stdpath.Join(d.cipher.EncryptDirName(dir), d.cipher.EncryptFileName(fileName))\n}\n"
  },
  {
    "path": "drivers/degoo/driver.go",
    "content": "package degoo\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Degoo struct {\n\tmodel.Storage\n\tAddition\n\tclient *http.Client\n}\n\nfunc (d *Degoo) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Degoo) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Degoo) Init(ctx context.Context) error {\n\n\td.client = base.HttpClient\n\n\t// Ensure we have a valid token (will login if needed or refresh if expired)\n\tif err := d.ensureValidToken(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize token: %w\", err)\n\t}\n\n\treturn d.getDevices(ctx)\n}\n\nfunc (d *Degoo) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Degoo) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\titems, err := d.getAllFileChildren5(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.MustSliceConvert(items, func(s DegooFileItem) model.Obj {\n\t\tisFolder := s.Category == 2 || s.Category == 1 || s.Category == 10\n\n\t\tcreateTime, modTime, _ := humanReadableTimes(s.CreationTime, s.LastModificationTime, s.LastUploadTime)\n\n\t\tsize, err := strconv.ParseInt(s.Size, 10, 64)\n\t\tif err != nil {\n\t\t\tsize = 0 // Default to 0 if size parsing fails\n\t\t}\n\n\t\treturn &model.Object{\n\t\t\tID:       s.ID,\n\t\t\tPath:     s.FilePath,\n\t\t\tName:     s.Name,\n\t\t\tSize:     size,\n\t\t\tModified: modTime,\n\t\t\tCtime:    createTime,\n\t\t\tIsFolder: isFolder,\n\t\t}\n\t}), nil\n}\n\nfunc (d *Degoo) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\titem, err := d.getOverlay4(ctx, file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{URL: item.URL}, nil\n}\n\nfunc (d *Degoo) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t// This is done by calling the setUploadFile3 API with a special checksum and size.\n\tconst query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) { setUploadFile3(Token: $Token, FileInfos: $FileInfos) }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\": d.AccessToken,\n\t\t\"FileInfos\": []map[string]interface{}{\n\t\t\t{\n\t\t\t\t\"Checksum\":     folderChecksum,\n\t\t\t\t\"Name\":         dirName,\n\t\t\t\t\"CreationTime\": time.Now().UnixMilli(),\n\t\t\t\t\"ParentID\":     parentDir.GetID(),\n\t\t\t\t\"Size\":         0,\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := d.apiCall(ctx, \"SetUploadFile3\", query, variables)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Degoo) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tconst query = `mutation SetMoveFile($Token: String!, $Copy: Boolean, $NewParentID: String!, $FileIDs: [String]!) { setMoveFile(Token: $Token, Copy: $Copy, NewParentID: $NewParentID, FileIDs: $FileIDs) }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\":       d.AccessToken,\n\t\t\"Copy\":        false,\n\t\t\"NewParentID\": dstDir.GetID(),\n\t\t\"FileIDs\":     []string{srcObj.GetID()},\n\t}\n\n\t_, err := d.apiCall(ctx, \"SetMoveFile\", query, variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn srcObj, nil\n}\n\nfunc (d *Degoo) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tconst query = `mutation SetRenameFile($Token: String!, $FileRenames: [FileRenameInfo]!) { setRenameFile(Token: $Token, FileRenames: $FileRenames) }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\": d.AccessToken,\n\t\t\"FileRenames\": []DegooFileRenameInfo{\n\t\t\t{\n\t\t\t\tID:      srcObj.GetID(),\n\t\t\t\tNewName: newName,\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := d.apiCall(ctx, \"SetRenameFile\", query, variables)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Degoo) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// Copy is not implemented, Degoo API does not support direct copy.\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Degoo) Remove(ctx context.Context, obj model.Obj) error {\n\t// Remove deletes a file or folder (moves to trash).\n\tconst query = `mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\":          d.AccessToken,\n\t\t\"IsInRecycleBin\": false,\n\t\t\"IDs\":            []map[string]string{{\"FileID\": obj.GetID()}},\n\t}\n\n\t_, err := d.apiCall(ctx, \"SetDeleteFile5\", query, variables)\n\treturn err\n}\n\nfunc (d *Degoo) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\ttmpF, err := file.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparentID := dstDir.GetID()\n\n\t// Calculate the checksum for the file.\n\tchecksum, err := d.checkSum(tmpF)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 1. Get upload authorization via getBucketWriteAuth4.\n\tauths, err := d.getBucketWriteAuth4(ctx, file, parentID, checksum)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 2. Upload file.\n\t// support rapid upload\n\tif auths.GetBucketWriteAuth4[0].Error != \"Already exist!\" {\n\t\terr = d.uploadS3(ctx, auths, tmpF, file, checksum)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 3. Register metadata with setUploadFile3.\n\tdata, err := d.SetUploadFile3(ctx, file, parentID, checksum)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !data.SetUploadFile3 {\n\t\treturn fmt.Errorf(\"setUploadFile3 failed: %v\", data)\n\t}\n\treturn nil\n}\n\nfunc (d *Degoo) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tquota, err := d.getUserInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(quota.GetUserInfo3.UsedQuota, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse used quota: %v\", err)\n\t}\n\ttotal, err := strconv.ParseInt(quota.GetUserInfo3.TotalQuota, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse total quota: %v\", err)\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/degoo/meta.go",
    "content": "package degoo\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername     string `json:\"username\" help:\"Your Degoo account email\"`\n\tPassword     string `json:\"password\" help:\"Your Degoo account password\"`\n\tRefreshToken string `json:\"refresh_token\" help:\"Refresh token for automatic token renewal, obtained automatically\"`\n\tAccessToken  string `json:\"access_token\" help:\"Access token for Degoo API, obtained automatically\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Degoo\",\n\tLocalSort:         true,\n\tDefaultRoot:       \"0\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Degoo{}\n\t})\n}\n"
  },
  {
    "path": "drivers/degoo/types.go",
    "content": "package degoo\n\nimport (\n\t\"encoding/json\"\n)\n\n// DegooLoginRequest represents the login request body.\ntype DegooLoginRequest struct {\n\tGenerateToken bool   `json:\"GenerateToken\"`\n\tUsername      string `json:\"Username\"`\n\tPassword      string `json:\"Password\"`\n}\n\n// DegooLoginResponse represents a successful login response.\ntype DegooLoginResponse struct {\n\tToken        string `json:\"Token\"`\n\tRefreshToken string `json:\"RefreshToken\"`\n}\n\n// DegooAccessTokenRequest represents the token refresh request body.\ntype DegooAccessTokenRequest struct {\n\tRefreshToken string `json:\"RefreshToken\"`\n}\n\n// DegooAccessTokenResponse represents the token refresh response.\ntype DegooAccessTokenResponse struct {\n\tAccessToken string `json:\"AccessToken\"`\n}\n\n// DegooFileItem represents a Degoo file or folder.\ntype DegooFileItem struct {\n\tID                   string `json:\"ID\"`\n\tParentID             string `json:\"ParentID\"`\n\tName                 string `json:\"Name\"`\n\tCategory             int    `json:\"Category\"`\n\tSize                 string `json:\"Size\"`\n\tURL                  string `json:\"URL\"`\n\tCreationTime         string `json:\"CreationTime\"`\n\tLastModificationTime string `json:\"LastModificationTime\"`\n\tLastUploadTime       string `json:\"LastUploadTime\"`\n\tMetadataID           string `json:\"MetadataID\"`\n\tDeviceID             int64  `json:\"DeviceID\"`\n\tFilePath             string `json:\"FilePath\"`\n\tIsInRecycleBin       bool   `json:\"IsInRecycleBin\"`\n}\n\ntype DegooErrors struct {\n\tPath      []string    `json:\"path\"`\n\tData      interface{} `json:\"data\"`\n\tErrorType string      `json:\"errorType\"`\n\tErrorInfo interface{} `json:\"errorInfo\"`\n\tMessage   string      `json:\"message\"`\n}\n\n// DegooGraphqlResponse is the common structure for GraphQL API responses.\ntype DegooGraphqlResponse struct {\n\tData   json.RawMessage `json:\"data\"`\n\tErrors []DegooErrors   `json:\"errors,omitempty\"`\n}\n\n// DegooGetChildren5Data is the data field for getFileChildren5.\ntype DegooGetChildren5Data struct {\n\tGetFileChildren5 struct {\n\t\tItems     []DegooFileItem `json:\"Items\"`\n\t\tNextToken string          `json:\"NextToken\"`\n\t} `json:\"getFileChildren5\"`\n}\n\n// DegooGetOverlay4Data is the data field for getOverlay4.\ntype DegooGetOverlay4Data struct {\n\tGetOverlay4 DegooFileItem `json:\"getOverlay4\"`\n}\n\n// DegooFileRenameInfo represents a file rename operation.\ntype DegooFileRenameInfo struct {\n\tID      string `json:\"ID\"`\n\tNewName string `json:\"NewName\"`\n}\n\n// DegooFileIDs represents a list of file IDs for move operations.\ntype DegooFileIDs struct {\n\tFileIDs []string `json:\"FileIDs\"`\n}\n\n// DegooGetBucketWriteAuth4Data is the data field for GetBucketWriteAuth4.\ntype DegooGetBucketWriteAuth4Data struct {\n\tGetBucketWriteAuth4 []struct {\n\t\tAuthData struct {\n\t\t\tPolicyBase64 string `json:\"PolicyBase64\"`\n\t\t\tSignature    string `json:\"Signature\"`\n\t\t\tBaseURL      string `json:\"BaseURL\"`\n\t\t\tKeyPrefix    string `json:\"KeyPrefix\"`\n\t\t\tAccessKey    struct {\n\t\t\t\tKey   string `json:\"Key\"`\n\t\t\t\tValue string `json:\"Value\"`\n\t\t\t} `json:\"AccessKey\"`\n\t\t\tACL            string `json:\"ACL\"`\n\t\t\tAdditionalBody []struct {\n\t\t\t\tKey   string `json:\"Key\"`\n\t\t\t\tValue string `json:\"Value\"`\n\t\t\t} `json:\"AdditionalBody\"`\n\t\t} `json:\"AuthData\"`\n\t\tError interface{} `json:\"Error\"`\n\t} `json:\"getBucketWriteAuth4\"`\n}\n\n// DegooSetUploadFile3Data is the data field for SetUploadFile3.\ntype DegooSetUploadFile3Data struct {\n\tSetUploadFile3 bool `json:\"setUploadFile3\"`\n}\n\ntype DegooGetUserInfo3Data struct {\n\tGetUserInfo3 struct {\n\t\t// ID string\n\t\t// FirstName string\n\t\t// LastName string\n\t\t// Email string\n\t\t// AvatarURL string\n\t\t// CountryCode string = CN\n\t\t// LanguageCode string = zh-cn\n\t\t// Phone string\n\t\t// AccountType int\n\t\tUsedQuota  string `json:\"UsedQuota\"`\n\t\tTotalQuota string `json:\"TotalQuota\"`\n\t\t// OAuth2Provider\n\t\t// GPMigrationStatus int\n\t\t// FeatureNoAds bool\n\t\t// FeatureTopSecret bool\n\t\t// FeatureDownsampling bool\n\t\t// FeatureAutomaticVideoUploads bool\n\t\t// FileSizeLimit string\n\t} `json:\"getUserInfo3\"`\n}\n"
  },
  {
    "path": "drivers/degoo/upload.go",
    "content": "package degoo\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc (d *Degoo) getBucketWriteAuth4(ctx context.Context, file model.FileStreamer, parentID string, checksum string) (*DegooGetBucketWriteAuth4Data, error) {\n\tconst query = `query GetBucketWriteAuth4(\n    $Token: String!\n    $ParentID: String!\n    $StorageUploadInfos: [StorageUploadInfo2]\n  ) {\n    getBucketWriteAuth4(\n      Token: $Token\n      ParentID: $ParentID\n      StorageUploadInfos: $StorageUploadInfos\n    ) {\n      AuthData {\n        PolicyBase64\n        Signature\n        BaseURL\n        KeyPrefix\n        AccessKey {\n          Key\n          Value\n        }\n        ACL\n        AdditionalBody {\n          Key\n          Value\n        }\n      }\n      Error\n    }\n  }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\":    d.AccessToken,\n\t\t\"ParentID\": parentID,\n\t\t\"StorageUploadInfos\": []map[string]string{{\n\t\t\t\"FileName\": file.GetName(),\n\t\t\t\"Checksum\": checksum,\n\t\t\t\"Size\":     strconv.FormatInt(file.GetSize(), 10),\n\t\t}}}\n\n\tdata, err := d.apiCall(ctx, \"GetBucketWriteAuth4\", query, variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp DegooGetBucketWriteAuth4Data\n\terr = json.Unmarshal(data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\n// checkSum calculates the SHA1-based checksum for Degoo upload API.\nfunc (d *Degoo) checkSum(file io.Reader) (string, error) {\n\tseed := []byte{13, 7, 2, 2, 15, 40, 75, 117, 13, 10, 19, 16, 29, 23, 3, 36}\n\thasher := sha1.New()\n\thasher.Write(seed)\n\n\tif _, err := utils.CopyWithBuffer(hasher, file); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcs := hasher.Sum(nil)\n\n\tcsBytes := []byte{10, byte(len(cs))}\n\tcsBytes = append(csBytes, cs...)\n\tcsBytes = append(csBytes, 16, 0)\n\n\treturn strings.ReplaceAll(base64.StdEncoding.EncodeToString(csBytes), \"/\", \"_\"), nil\n}\n\nfunc (d *Degoo) uploadS3(ctx context.Context, auths *DegooGetBucketWriteAuth4Data, tmpF model.File, file model.FileStreamer, checksum string) error {\n\ta := auths.GetBucketWriteAuth4[0].AuthData\n\n\t_, err := tmpF.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\text := utils.Ext(file.GetName())\n\tkey := fmt.Sprintf(\"%s%s/%s.%s\", a.KeyPrefix, ext, checksum, ext)\n\n\tvar b bytes.Buffer\n\tw := multipart.NewWriter(&b)\n\terr = w.WriteField(\"key\", key)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = w.WriteField(\"acl\", a.ACL)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = w.WriteField(\"policy\", a.PolicyBase64)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = w.WriteField(\"signature\", a.Signature)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = w.WriteField(a.AccessKey.Key, a.AccessKey.Value)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, additional := range a.AdditionalBody {\n\t\terr = w.WriteField(additional.Key, additional.Value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\terr = w.WriteField(\"Content-Type\", \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.CreateFormFile(\"file\", key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theadSize := b.Len()\n\terr = w.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\thead := bytes.NewReader(b.Bytes()[:headSize])\n\ttail := bytes.NewReader(b.Bytes()[headSize:])\n\n\trateLimitedRd := driver.NewLimitedUploadStream(ctx, io.MultiReader(head, tmpF, tail))\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, a.BaseURL, rateLimitedRd)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Add(\"ngsw-bypass\", \"1\")\n\treq.Header.Add(\"Content-Type\", w.FormDataContentType())\n\n\tres, err := d.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusNoContent {\n\t\treturn fmt.Errorf(\"upload failed with status code %d\", res.StatusCode)\n\t}\n\treturn nil\n}\n\nvar _ driver.Driver = (*Degoo)(nil)\n\nfunc (d *Degoo) SetUploadFile3(ctx context.Context, file model.FileStreamer, parentID string, checksum string) (*DegooSetUploadFile3Data, error) {\n\tconst query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) {\n    setUploadFile3(Token: $Token, FileInfos: $FileInfos)\n  }`\n\n\tvariables := map[string]interface{}{\n\t\t\"Token\": d.AccessToken,\n\t\t\"FileInfos\": []map[string]string{{\n\t\t\t\"Checksum\":     checksum,\n\t\t\t\"CreationTime\": strconv.FormatInt(file.CreateTime().UnixMilli(), 10),\n\t\t\t\"Name\":         file.GetName(),\n\t\t\t\"ParentID\":     parentID,\n\t\t\t\"Size\":         strconv.FormatInt(file.GetSize(), 10),\n\t\t}}}\n\n\tdata, err := d.apiCall(ctx, \"SetUploadFile3\", query, variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp DegooSetUploadFile3Data\n\terr = json.Unmarshal(data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/degoo/util.go",
    "content": "package degoo\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\n// Thanks to https://github.com/bernd-wechner/Degoo for API research.\n\nconst (\n\t// API endpoints\n\tloginURL       = \"https://rest-api.degoo.com/login\"\n\taccessTokenURL = \"https://rest-api.degoo.com/access-token/v2\"\n\tapiURL         = \"https://production-appsync.degoo.com/graphql\"\n\n\t// API configuration\n\tapiKey         = \"da2-vs6twz5vnjdavpqndtbzg3prra\"\n\tfolderChecksum = \"CgAQAg\"\n\n\t// Token management\n\ttokenRefreshThreshold = 5 * time.Minute\n\n\t// Rate limiting\n\tminRequestInterval = 1 * time.Second\n\n\t// Error messages\n\terrRateLimited  = \"rate limited (429), please try again later\"\n\terrUnauthorized = \"unauthorized access\"\n)\n\nvar (\n\t// Global rate limiting - protects against concurrent API calls\n\tlastRequestTime time.Time\n\trequestMutex    sync.Mutex\n)\n\n// JWT payload structure for token expiration checking\ntype JWTPayload struct {\n\tUserID string `json:\"userID\"`\n\tExp    int64  `json:\"exp\"`\n\tIat    int64  `json:\"iat\"`\n}\n\n// Rate limiting helper functions\n\n// applyRateLimit ensures minimum interval between API requests\nfunc applyRateLimit() {\n\trequestMutex.Lock()\n\tdefer requestMutex.Unlock()\n\n\tif !lastRequestTime.IsZero() {\n\t\tif elapsed := time.Since(lastRequestTime); elapsed < minRequestInterval {\n\t\t\ttime.Sleep(minRequestInterval - elapsed)\n\t\t}\n\t}\n\tlastRequestTime = time.Now()\n}\n\n// HTTP request helper functions\n\n// createJSONRequest creates a new HTTP request with JSON body\nfunc createJSONRequest(ctx context.Context, method, url string, body interface{}) (*http.Request, error) {\n\tjsonBody, err := json.Marshal(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(jsonBody))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", base.UserAgent)\n\treturn req, nil\n}\n\n// checkHTTPResponse checks for common HTTP error conditions\nfunc checkHTTPResponse(resp *http.Response, operation string) error {\n\tif resp.StatusCode == http.StatusTooManyRequests {\n\t\treturn fmt.Errorf(\"%s %s\", operation, errRateLimited)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"%s failed: %s\", operation, resp.Status)\n\t}\n\treturn nil\n}\n\n// isTokenExpired checks if the JWT token is expired or will expire soon\nfunc (d *Degoo) isTokenExpired() bool {\n\tif d.AccessToken == \"\" {\n\t\treturn true\n\t}\n\n\tpayload, err := extractJWTPayload(d.AccessToken)\n\tif err != nil {\n\t\treturn true // Invalid token format\n\t}\n\n\t// Check if token expires within the threshold\n\texpireTime := time.Unix(payload.Exp, 0)\n\treturn time.Now().Add(tokenRefreshThreshold).After(expireTime)\n}\n\n// extractJWTPayload extracts and parses JWT payload\nfunc extractJWTPayload(token string) (*JWTPayload, error) {\n\tparts := strings.Split(token, \".\")\n\tif len(parts) != 3 {\n\t\treturn nil, fmt.Errorf(\"invalid JWT format\")\n\t}\n\n\t// Decode the payload (second part)\n\tpayload, err := base64.RawURLEncoding.DecodeString(parts[1])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode JWT payload: %w\", err)\n\t}\n\n\tvar jwtPayload JWTPayload\n\tif err := json.Unmarshal(payload, &jwtPayload); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JWT payload: %w\", err)\n\t}\n\n\treturn &jwtPayload, nil\n}\n\n// refreshToken attempts to refresh the access token using the refresh token\nfunc (d *Degoo) refreshToken(ctx context.Context) error {\n\tif d.RefreshToken == \"\" {\n\t\treturn fmt.Errorf(\"no refresh token available\")\n\t}\n\n\t// Create request\n\ttokenReq := DegooAccessTokenRequest{RefreshToken: d.RefreshToken}\n\treq, err := createJSONRequest(ctx, \"POST\", accessTokenURL, tokenReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create refresh token request: %w\", err)\n\t}\n\n\t// Execute request\n\tresp, err := d.client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"refresh token request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Check response\n\tif err := checkHTTPResponse(resp, \"refresh token\"); err != nil {\n\t\treturn err\n\t}\n\n\tvar accessTokenResp DegooAccessTokenResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&accessTokenResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse access token response: %w\", err)\n\t}\n\n\tif accessTokenResp.AccessToken == \"\" {\n\t\treturn fmt.Errorf(\"empty access token received\")\n\t}\n\n\td.AccessToken = accessTokenResp.AccessToken\n\t// Save the updated token to storage\n\top.MustSaveDriverStorage(d)\n\n\treturn nil\n}\n\n// ensureValidToken ensures we have a valid, non-expired token\nfunc (d *Degoo) ensureValidToken(ctx context.Context) error {\n\t// Check if token is expired or will expire soon\n\tif d.isTokenExpired() {\n\t\t// Try to refresh token first if we have a refresh token\n\t\tif d.RefreshToken != \"\" {\n\t\t\tif refreshErr := d.refreshToken(ctx); refreshErr == nil {\n\t\t\t\treturn nil // Successfully refreshed\n\t\t\t} else {\n\t\t\t\t// If refresh failed, fall back to full login\n\t\t\t\tfmt.Printf(\"Token refresh failed, falling back to full login: %v\\n\", refreshErr)\n\t\t\t}\n\t\t}\n\n\t\t// Perform full login\n\t\tif d.Username != \"\" && d.Password != \"\" {\n\t\t\treturn d.login(ctx)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// login performs the login process and retrieves the access token.\nfunc (d *Degoo) login(ctx context.Context) error {\n\tif d.Username == \"\" || d.Password == \"\" {\n\t\treturn fmt.Errorf(\"username or password not provided\")\n\t}\n\n\tcreds := DegooLoginRequest{\n\t\tGenerateToken: true,\n\t\tUsername:      d.Username,\n\t\tPassword:      d.Password,\n\t}\n\n\tjsonCreds, err := json.Marshal(creds)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to serialize login credentials: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", loginURL, bytes.NewBuffer(jsonCreds))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create login request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", base.UserAgent)\n\treq.Header.Set(\"Origin\", \"https://app.degoo.com\")\n\n\tresp, err := d.client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"login request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Handle rate limiting (429 Too Many Requests)\n\tif resp.StatusCode == http.StatusTooManyRequests {\n\t\treturn fmt.Errorf(\"login rate limited (429), please try again later\")\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"login failed: %s\", resp.Status)\n\t}\n\n\tvar loginResp DegooLoginResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse login response: %w\", err)\n\t}\n\n\tif loginResp.RefreshToken != \"\" {\n\t\ttokenReq := DegooAccessTokenRequest{RefreshToken: loginResp.RefreshToken}\n\t\tjsonTokenReq, err := json.Marshal(tokenReq)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to serialize access token request: %w\", err)\n\t\t}\n\n\t\ttokenReqHTTP, err := http.NewRequestWithContext(ctx, \"POST\", accessTokenURL, bytes.NewBuffer(jsonTokenReq))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create access token request: %w\", err)\n\t\t}\n\n\t\ttokenReqHTTP.Header.Set(\"User-Agent\", base.UserAgent)\n\n\t\ttokenResp, err := d.client.Do(tokenReqHTTP)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get access token: %w\", err)\n\t\t}\n\t\tdefer tokenResp.Body.Close()\n\n\t\tvar accessTokenResp DegooAccessTokenResponse\n\t\tif err := json.NewDecoder(tokenResp.Body).Decode(&accessTokenResp); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse access token response: %w\", err)\n\t\t}\n\t\td.AccessToken = accessTokenResp.AccessToken\n\t\td.RefreshToken = loginResp.RefreshToken // Save refresh token\n\t} else if loginResp.Token != \"\" {\n\t\td.AccessToken = loginResp.Token\n\t\td.RefreshToken = \"\" // Direct token, no refresh token available\n\t} else {\n\t\treturn fmt.Errorf(\"login failed, no valid token returned\")\n\t}\n\n\t// Save the updated tokens to storage\n\top.MustSaveDriverStorage(d)\n\n\treturn nil\n}\n\n// apiCall performs a Degoo GraphQL API request.\nfunc (d *Degoo) apiCall(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {\n\t// Apply rate limiting\n\tapplyRateLimit()\n\n\t// Ensure we have a valid token before making the API call\n\tif err := d.ensureValidToken(ctx); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ensure valid token: %w\", err)\n\t}\n\n\t// Update the Token in variables if it exists (after potential refresh)\n\td.updateTokenInVariables(variables)\n\n\treturn d.executeGraphQLRequest(ctx, operationName, query, variables)\n}\n\n// updateTokenInVariables updates the Token field in GraphQL variables\nfunc (d *Degoo) updateTokenInVariables(variables map[string]interface{}) {\n\tif variables != nil {\n\t\tif _, hasToken := variables[\"Token\"]; hasToken {\n\t\t\tvariables[\"Token\"] = d.AccessToken\n\t\t}\n\t}\n}\n\n// executeGraphQLRequest executes a GraphQL request with retry logic\nfunc (d *Degoo) executeGraphQLRequest(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {\n\treqBody := map[string]interface{}{\n\t\t\"operationName\": operationName,\n\t\t\"query\":         query,\n\t\t\"variables\":     variables,\n\t}\n\n\t// Create and configure request\n\treq, err := createJSONRequest(ctx, \"POST\", apiURL, reqBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set Degoo-specific headers\n\treq.Header.Set(\"x-api-key\", apiKey)\n\tif d.AccessToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", d.AccessToken))\n\t}\n\n\t// Execute request\n\tresp, err := d.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GraphQL API request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Check for HTTP errors\n\tif err := checkHTTPResponse(resp, \"GraphQL API\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse GraphQL response\n\tvar degooResp DegooGraphqlResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&degooResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode GraphQL response: %w\", err)\n\t}\n\n\t// Handle GraphQL errors\n\tif len(degooResp.Errors) > 0 {\n\t\treturn d.handleGraphQLError(ctx, degooResp.Errors[0], operationName, query, variables)\n\t}\n\n\treturn degooResp.Data, nil\n}\n\n// handleGraphQLError handles GraphQL-level errors with retry logic\nfunc (d *Degoo) handleGraphQLError(ctx context.Context, gqlError DegooErrors, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {\n\tif gqlError.ErrorType == \"Unauthorized\" {\n\t\t// Re-login and retry\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%s, login failed: %w\", errUnauthorized, err)\n\t\t}\n\n\t\t// Update token in variables and retry\n\t\td.updateTokenInVariables(variables)\n\t\treturn d.apiCall(ctx, operationName, query, variables)\n\t}\n\n\treturn nil, fmt.Errorf(\"GraphQL API error: %s\", gqlError.Message)\n}\n\n// humanReadableTimes converts Degoo timestamps to Go time.Time.\nfunc humanReadableTimes(creation, modification, upload string) (cTime, mTime, uTime time.Time) {\n\tcTime, _ = time.Parse(time.RFC3339, creation)\n\tif modification != \"\" {\n\t\tmodMillis, _ := strconv.ParseInt(modification, 10, 64)\n\t\tmTime = time.Unix(0, modMillis*int64(time.Millisecond))\n\t}\n\tif upload != \"\" {\n\t\tupMillis, _ := strconv.ParseInt(upload, 10, 64)\n\t\tuTime = time.Unix(0, upMillis*int64(time.Millisecond))\n\t}\n\treturn cTime, mTime, uTime\n}\n\n// getDevices fetches and caches top-level devices and folders.\nfunc (d *Degoo) getDevices(ctx context.Context) error {\n\tconst query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ParentID } NextToken } }`\n\tvariables := map[string]interface{}{\n\t\t\"Token\":    d.AccessToken,\n\t\t\"ParentID\": \"0\",\n\t\t\"Limit\":    10,\n\t\t\"Order\":    3,\n\t}\n\tdata, err := d.apiCall(ctx, \"GetFileChildren5\", query, variables)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar resp DegooGetChildren5Data\n\tif err := json.Unmarshal(data, &resp); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse device list: %w\", err)\n\t}\n\tif d.RootFolderID == \"0\" {\n\t\tif len(resp.GetFileChildren5.Items) > 0 {\n\t\t\td.RootFolderID = resp.GetFileChildren5.Items[0].ParentID\n\t\t}\n\t\top.MustSaveDriverStorage(d)\n\t}\n\treturn nil\n}\n\n// getAllFileChildren5 fetches all children of a directory with pagination.\nfunc (d *Degoo) getAllFileChildren5(ctx context.Context, parentID string) ([]DegooFileItem, error) {\n\tconst query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime FilePath IsInRecycleBin DeviceID MetadataID } NextToken } }`\n\tvar allItems []DegooFileItem\n\tnextToken := \"\"\n\tfor {\n\t\tvariables := map[string]interface{}{\n\t\t\t\"Token\":    d.AccessToken,\n\t\t\t\"ParentID\": parentID,\n\t\t\t\"Limit\":    1000,\n\t\t\t\"Order\":    3,\n\t\t}\n\t\tif nextToken != \"\" {\n\t\t\tvariables[\"NextToken\"] = nextToken\n\t\t}\n\t\tdata, err := d.apiCall(ctx, \"GetFileChildren5\", query, variables)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar resp DegooGetChildren5Data\n\t\tif err := json.Unmarshal(data, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tallItems = append(allItems, resp.GetFileChildren5.Items...)\n\t\tif resp.GetFileChildren5.NextToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tnextToken = resp.GetFileChildren5.NextToken\n\t}\n\treturn allItems, nil\n}\n\n// getOverlay4 fetches metadata for a single item by ID.\nfunc (d *Degoo) getOverlay4(ctx context.Context, id string) (DegooFileItem, error) {\n\tconst query = `query GetOverlay4($Token: String!, $ID: IDType!) { getOverlay4(Token: $Token, ID: $ID) { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime URL FilePath IsInRecycleBin DeviceID MetadataID } }`\n\tvariables := map[string]interface{}{\n\t\t\"Token\": d.AccessToken,\n\t\t\"ID\": map[string]string{\n\t\t\t\"FileID\": id,\n\t\t},\n\t}\n\tdata, err := d.apiCall(ctx, \"GetOverlay4\", query, variables)\n\tif err != nil {\n\t\treturn DegooFileItem{}, err\n\t}\n\tvar resp DegooGetOverlay4Data\n\tif err := json.Unmarshal(data, &resp); err != nil {\n\t\treturn DegooFileItem{}, fmt.Errorf(\"failed to parse item metadata: %w\", err)\n\t}\n\treturn resp.GetOverlay4, nil\n}\n\nfunc (d *Degoo) getUserInfo(ctx context.Context) (DegooGetUserInfo3Data, error) {\n\tconst query = \"query GetUserInfo3($Token: String!) { getUserInfo3(Token: $Token) { UsedQuota TotalQuota } }\"\n\tvariables := map[string]interface{}{\n\t\t\"Token\": d.AccessToken,\n\t}\n\tdata, err := d.apiCall(ctx, \"GetUserInfo3\", query, variables)\n\tvar resp DegooGetUserInfo3Data\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tif err = json.Unmarshal(data, &resp); err != nil {\n\t\treturn resp, fmt.Errorf(\"failed to parse user info: %w\", err)\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "drivers/doubao/driver.go",
    "content": "package doubao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Doubao struct {\n\tmodel.Storage\n\tAddition\n\t*UploadToken\n\tUserId       string\n\tuploadThread int\n\tlimiter      *rate.Limiter\n}\n\nfunc (d *Doubao) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Doubao) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Doubao) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\tuploadThread, err := strconv.Atoi(d.UploadThread)\n\tif err != nil || uploadThread < 1 {\n\t\td.uploadThread, d.UploadThread = 3, \"3\" // Set default value\n\t} else {\n\t\td.uploadThread = uploadThread\n\t}\n\n\tif d.UserId == \"\" {\n\t\tuserInfo, err := d.getUserInfo()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\td.UserId = strconv.FormatInt(userInfo.UserID, 10)\n\t}\n\n\tif d.UploadToken == nil {\n\t\tuploadToken, err := d.initUploadToken()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\td.UploadToken = uploadToken\n\t}\n\n\tif d.LimitRate > 0 {\n\t\td.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Doubao) WaitLimit(ctx context.Context) error {\n\tif d.limiter != nil {\n\t\treturn d.limiter.Wait(ctx)\n\t}\n\treturn nil\n}\n\nfunc (d *Doubao) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Doubao) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar files []model.Obj\n\tfileList, err := d.getFiles(dir.GetID(), \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, child := range fileList {\n\t\tfiles = append(files, &Object{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       child.ID,\n\t\t\t\tPath:     child.ParentID,\n\t\t\t\tName:     child.Name,\n\t\t\t\tSize:     child.Size,\n\t\t\t\tModified: time.Unix(child.UpdateTime, 0),\n\t\t\t\tCtime:    time.Unix(child.CreateTime, 0),\n\t\t\t\tIsFolder: child.NodeType == 1,\n\t\t\t},\n\t\t\tKey:      child.Key,\n\t\t\tNodeType: child.NodeType,\n\t\t})\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *Doubao) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar downloadUrl string\n\n\tif u, ok := file.(*Object); ok {\n\t\tswitch d.DownloadApi {\n\t\tcase \"get_download_info\":\n\t\t\tvar r GetDownloadInfoResp\n\t\t\t_, err := d.request(\"/samantha/aispace/get_download_info\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\"requests\": []base.Json{{\"node_id\": file.GetID()}},\n\t\t\t\t})\n\t\t\t}, &r)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdownloadUrl = r.Data.DownloadInfos[0].MainURL\n\t\tcase \"get_file_url\":\n\t\t\tswitch u.NodeType {\n\t\t\tcase VideoType, AudioType:\n\t\t\t\tvar r GetVideoFileUrlResp\n\t\t\t\t_, err := d.request(\"/samantha/media/get_play_info\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\t\"key\":     u.Key,\n\t\t\t\t\t\t\"node_id\": file.GetID(),\n\t\t\t\t\t})\n\t\t\t\t}, &r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdownloadUrl = r.Data.OriginalMediaInfo.MainURL\n\t\t\tdefault:\n\t\t\t\tvar r GetFileUrlResp\n\t\t\t\t_, err := d.request(\"/alice/message/get_file_url\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\t\"uris\": []string{u.Key},\n\t\t\t\t\t\t\"type\": FileNodeType[u.NodeType],\n\t\t\t\t\t})\n\t\t\t\t}, &r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdownloadUrl = r.Data.FileUrls[0].MainURL\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\n\t\t// 生成标准的Content-Disposition\n\t\tcontentDisposition := utils.GenerateContentDisposition(u.Name)\n\n\t\treturn &model.Link{\n\t\t\tURL: downloadUrl,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"User-Agent\":          []string{UserAgent},\n\t\t\t\t\"Content-Disposition\": []string{contentDisposition},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\treturn nil, errors.New(\"can't convert obj to URL\")\n}\n\nfunc (d *Doubao) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tvar r UploadNodeResp\n\t_, err := d.request(\"/samantha/aispace/upload_node\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"node_list\": []base.Json{\n\t\t\t\t{\n\t\t\t\t\t\"local_id\":  uuid.New().String(),\n\t\t\t\t\t\"name\":      dirName,\n\t\t\t\t\t\"parent_id\": parentDir.GetID(),\n\t\t\t\t\t\"node_type\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}, &r)\n\treturn err\n}\n\nfunc (d *Doubao) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tvar r UploadNodeResp\n\t_, err := d.request(\"/samantha/aispace/move_node\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"node_list\": []base.Json{\n\t\t\t\t{\"id\": srcObj.GetID()},\n\t\t\t},\n\t\t\t\"current_parent_id\": srcObj.GetPath(),\n\t\t\t\"target_parent_id\":  dstDir.GetID(),\n\t\t})\n\t}, &r)\n\treturn err\n}\n\nfunc (d *Doubao) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tvar r BaseResp\n\t_, err := d.request(\"/samantha/aispace/rename_node\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"node_id\":   srcObj.GetID(),\n\t\t\t\"node_name\": newName,\n\t\t})\n\t}, &r)\n\treturn err\n}\n\nfunc (d *Doubao) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO copy obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Doubao) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tvar r BaseResp\n\t_, err := d.request(\"/samantha/aispace/delete_node\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\"node_list\": []base.Json{{\"id\": obj.GetID()}}})\n\t}, &r)\n\treturn err\n}\n\nfunc (d *Doubao) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 根据MIME类型确定数据类型\n\tmimetype := file.GetMimetype()\n\tdataType := FileDataType\n\n\tswitch {\n\tcase strings.HasPrefix(mimetype, \"video/\"):\n\t\tdataType = VideoDataType\n\tcase strings.HasPrefix(mimetype, \"audio/\"):\n\t\tdataType = VideoDataType // 音频与视频使用相同的处理方式\n\tcase strings.HasPrefix(mimetype, \"image/\"):\n\t\tdataType = ImgDataType\n\t}\n\n\t// 获取上传配置\n\tuploadConfig := UploadConfig{}\n\tif err := d.getUploadConfig(&uploadConfig, dataType, file); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 根据文件大小选择上传方式\n\tif file.GetSize() <= 1*utils.MB { // 小于1MB，使用普通模式上传\n\t\treturn d.Upload(ctx, &uploadConfig, dstDir, file, up, dataType)\n\t}\n\t// 大文件使用分片上传\n\treturn d.UploadByMultipart(ctx, &uploadConfig, file.GetSize(), dstDir, file, up, dataType)\n}\n\nfunc (d *Doubao) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Doubao) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Doubao) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Doubao) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n\t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n\t// return errs.NotImplement to use an internal archive tool\n\treturn nil, errs.NotImplement\n}\n\n//func (d *Doubao) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Doubao)(nil)\n"
  },
  {
    "path": "drivers/doubao/meta.go",
    "content": "package doubao\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\t// driver.RootPath\n\tdriver.RootID\n\t// define other\n\tCookie       string  `json:\"cookie\" type:\"text\"`\n\tUploadThread string  `json:\"upload_thread\" default:\"3\"`\n\tDownloadApi  string  `json:\"download_api\" type:\"select\" options:\"get_file_url,get_download_info\" default:\"get_file_url\"`\n\tLimitRate    float64 `json:\"limit_rate\" type:\"float\" default:\"2\" help:\"limit all api request rate ([limit]r/1s)\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Doubao\",\n\tLocalSort:   true,\n\tDefaultRoot: \"0\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Doubao{\n\t\t\tAddition: Addition{\n\t\t\t\tLimitRate: 2,\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/doubao/types.go",
    "content": "package doubao\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype BaseResp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n}\n\ntype NodeInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tNodeInfo   File   `json:\"node_info\"`\n\t\tChildren   []File `json:\"children\"`\n\t\tNextCursor string `json:\"next_cursor\"`\n\t\tHasMore    bool   `json:\"has_more\"`\n\t} `json:\"data\"`\n}\n\ntype File struct {\n\tID                  string `json:\"id\"`\n\tName                string `json:\"name\"`\n\tKey                 string `json:\"key\"`\n\tNodeType            int    `json:\"node_type\"` // 0: 文件, 1: 文件夹\n\tSize                int64  `json:\"size\"`\n\tSource              int    `json:\"source\"`\n\tNameReviewStatus    int    `json:\"name_review_status\"`\n\tContentReviewStatus int    `json:\"content_review_status\"`\n\tRiskReviewStatus    int    `json:\"risk_review_status\"`\n\tConversationID      string `json:\"conversation_id\"`\n\tParentID            string `json:\"parent_id\"`\n\tCreateTime          int64  `json:\"create_time\"`\n\tUpdateTime          int64  `json:\"update_time\"`\n}\n\ntype GetDownloadInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tDownloadInfos []struct {\n\t\t\tNodeID    string `json:\"node_id\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"download_infos\"`\n\t} `json:\"data\"`\n}\n\ntype GetFileUrlResp struct {\n\tBaseResp\n\tData struct {\n\t\tFileUrls []struct {\n\t\t\tURI     string `json:\"uri\"`\n\t\t\tMainURL string `json:\"main_url\"`\n\t\t\tBackURL string `json:\"back_url\"`\n\t\t} `json:\"file_urls\"`\n\t} `json:\"data\"`\n}\n\ntype GetVideoFileUrlResp struct {\n\tBaseResp\n\tData struct {\n\t\tMediaType string `json:\"media_type\"`\n\t\tMediaInfo []struct {\n\t\t\tMeta struct {\n\t\t\t\tHeight     string  `json:\"height\"`\n\t\t\t\tWidth      string  `json:\"width\"`\n\t\t\t\tFormat     string  `json:\"format\"`\n\t\t\t\tDuration   float64 `json:\"duration\"`\n\t\t\t\tCodecType  string  `json:\"codec_type\"`\n\t\t\t\tDefinition string  `json:\"definition\"`\n\t\t\t} `json:\"meta\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"media_info\"`\n\t\tOriginalMediaInfo struct {\n\t\t\tMeta struct {\n\t\t\t\tHeight     string  `json:\"height\"`\n\t\t\t\tWidth      string  `json:\"width\"`\n\t\t\t\tFormat     string  `json:\"format\"`\n\t\t\t\tDuration   float64 `json:\"duration\"`\n\t\t\t\tCodecType  string  `json:\"codec_type\"`\n\t\t\t\tDefinition string  `json:\"definition\"`\n\t\t\t} `json:\"meta\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"original_media_info\"`\n\t\tPosterURL      string `json:\"poster_url\"`\n\t\tPlayableStatus int    `json:\"playable_status\"`\n\t} `json:\"data\"`\n}\n\ntype UploadNodeResp struct {\n\tBaseResp\n\tData struct {\n\t\tNodeList []struct {\n\t\t\tLocalID  string `json:\"local_id\"`\n\t\t\tID       string `json:\"id\"`\n\t\t\tParentID string `json:\"parent_id\"`\n\t\t\tName     string `json:\"name\"`\n\t\t\tKey      string `json:\"key\"`\n\t\t\tNodeType int    `json:\"node_type\"` // 0: 文件, 1: 文件夹\n\t\t} `json:\"node_list\"`\n\t} `json:\"data\"`\n}\n\ntype Object struct {\n\tmodel.Object\n\tKey      string\n\tNodeType int\n}\n\ntype UserInfoResp struct {\n\tData    UserInfo `json:\"data\"`\n\tMessage string   `json:\"message\"`\n}\ntype AppUserInfo struct {\n\tBuiAuditInfo string `json:\"bui_audit_info\"`\n}\ntype AuditInfo struct {\n}\ntype Details struct {\n}\ntype BuiAuditInfo struct {\n\tAuditInfo      AuditInfo `json:\"audit_info\"`\n\tIsAuditing     bool      `json:\"is_auditing\"`\n\tAuditStatus    int       `json:\"audit_status\"`\n\tLastUpdateTime int64     `json:\"last_update_time\"`\n\tUnpassReason   string    `json:\"unpass_reason\"`\n\tDetails        Details   `json:\"details\"`\n}\ntype Connects struct {\n\tPlatform           string `json:\"platform\"`\n\tProfileImageURL    string `json:\"profile_image_url\"`\n\tExpiredTime        int    `json:\"expired_time\"`\n\tExpiresIn          int    `json:\"expires_in\"`\n\tPlatformScreenName string `json:\"platform_screen_name\"`\n\tUserID             int64  `json:\"user_id\"`\n\tPlatformUID        string `json:\"platform_uid\"`\n\tSecPlatformUID     string `json:\"sec_platform_uid\"`\n\tPlatformAppID      int    `json:\"platform_app_id\"`\n\tModifyTime         int    `json:\"modify_time\"`\n\tAccessToken        string `json:\"access_token\"`\n\tOpenID             string `json:\"open_id\"`\n}\ntype OperStaffRelationInfo struct {\n\tHasPassword               int    `json:\"has_password\"`\n\tMobile                    string `json:\"mobile\"`\n\tSecOperStaffUserID        string `json:\"sec_oper_staff_user_id\"`\n\tRelationMobileCountryCode int    `json:\"relation_mobile_country_code\"`\n}\ntype UserInfo struct {\n\tAppID                 int                   `json:\"app_id\"`\n\tAppUserInfo           AppUserInfo           `json:\"app_user_info\"`\n\tAvatarURL             string                `json:\"avatar_url\"`\n\tBgImgURL              string                `json:\"bg_img_url\"`\n\tBuiAuditInfo          BuiAuditInfo          `json:\"bui_audit_info\"`\n\tCanBeFoundByPhone     int                   `json:\"can_be_found_by_phone\"`\n\tConnects              []Connects            `json:\"connects\"`\n\tCountryCode           int                   `json:\"country_code\"`\n\tDescription           string                `json:\"description\"`\n\tDeviceID              int                   `json:\"device_id\"`\n\tEmail                 string                `json:\"email\"`\n\tEmailCollected        bool                  `json:\"email_collected\"`\n\tGender                int                   `json:\"gender\"`\n\tHasPassword           int                   `json:\"has_password\"`\n\tHmRegion              int                   `json:\"hm_region\"`\n\tIsBlocked             int                   `json:\"is_blocked\"`\n\tIsBlocking            int                   `json:\"is_blocking\"`\n\tIsRecommendAllowed    int                   `json:\"is_recommend_allowed\"`\n\tIsVisitorAccount      bool                  `json:\"is_visitor_account\"`\n\tMobile                string                `json:\"mobile\"`\n\tName                  string                `json:\"name\"`\n\tNeedCheckBindStatus   bool                  `json:\"need_check_bind_status\"`\n\tOdinUserType          int                   `json:\"odin_user_type\"`\n\tOperStaffRelationInfo OperStaffRelationInfo `json:\"oper_staff_relation_info\"`\n\tPhoneCollected        bool                  `json:\"phone_collected\"`\n\tRecommendHintMessage  string                `json:\"recommend_hint_message\"`\n\tScreenName            string                `json:\"screen_name\"`\n\tSecUserID             string                `json:\"sec_user_id\"`\n\tSessionKey            string                `json:\"session_key\"`\n\tUseHmRegion           bool                  `json:\"use_hm_region\"`\n\tUserCreateTime        int64                 `json:\"user_create_time\"`\n\tUserID                int64                 `json:\"user_id\"`\n\tUserIDStr             string                `json:\"user_id_str\"`\n\tUserVerified          bool                  `json:\"user_verified\"`\n\tVerifiedContent       string                `json:\"verified_content\"`\n}\n\n// UploadToken 上传令牌配置\ntype UploadToken struct {\n\tAlice    map[string]UploadAuthToken\n\tSamantha MediaUploadAuthToken\n}\n\n// UploadAuthToken 多种类型的上传配置：图片/文件\ntype UploadAuthToken struct {\n\tServiceID        string `json:\"service_id\"`\n\tUploadPathPrefix string `json:\"upload_path_prefix\"`\n\tAuth             struct {\n\t\tAccessKeyID     string    `json:\"access_key_id\"`\n\t\tSecretAccessKey string    `json:\"secret_access_key\"`\n\t\tSessionToken    string    `json:\"session_token\"`\n\t\tExpiredTime     time.Time `json:\"expired_time\"`\n\t\tCurrentTime     time.Time `json:\"current_time\"`\n\t} `json:\"auth\"`\n\tUploadHost string `json:\"upload_host\"`\n}\n\n// MediaUploadAuthToken 媒体上传配置\ntype MediaUploadAuthToken struct {\n\tStsToken struct {\n\t\tAccessKeyID     string    `json:\"access_key_id\"`\n\t\tSecretAccessKey string    `json:\"secret_access_key\"`\n\t\tSessionToken    string    `json:\"session_token\"`\n\t\tExpiredTime     time.Time `json:\"expired_time\"`\n\t\tCurrentTime     time.Time `json:\"current_time\"`\n\t} `json:\"sts_token\"`\n\tUploadInfo struct {\n\t\tVideoHost string `json:\"video_host\"`\n\t\tSpaceName string `json:\"space_name\"`\n\t} `json:\"upload_info\"`\n}\n\ntype UploadAuthTokenResp struct {\n\tBaseResp\n\tData UploadAuthToken `json:\"data\"`\n}\n\ntype MediaUploadAuthTokenResp struct {\n\tBaseResp\n\tData MediaUploadAuthToken `json:\"data\"`\n}\n\ntype ResponseMetadata struct {\n\tRequestID string `json:\"RequestId\"`\n\tAction    string `json:\"Action\"`\n\tVersion   string `json:\"Version\"`\n\tService   string `json:\"Service\"`\n\tRegion    string `json:\"Region\"`\n\tError     struct {\n\t\tCodeN   int    `json:\"CodeN,omitempty\"`\n\t\tCode    string `json:\"Code,omitempty\"`\n\t\tMessage string `json:\"Message,omitempty\"`\n\t} `json:\"Error,omitempty\"`\n}\n\ntype UploadConfig struct {\n\tUploadAddress         UploadAddress         `json:\"UploadAddress\"`\n\tFallbackUploadAddress FallbackUploadAddress `json:\"FallbackUploadAddress\"`\n\tInnerUploadAddress    InnerUploadAddress    `json:\"InnerUploadAddress\"`\n\tRequestID             string                `json:\"RequestId\"`\n\tSDKParam              interface{}           `json:\"SDKParam\"`\n}\n\ntype UploadConfigResp struct {\n\tResponseMetadata `json:\"ResponseMetadata\"`\n\tResult           UploadConfig `json:\"Result\"`\n}\n\n// StoreInfo 存储信息\ntype StoreInfo struct {\n\tStoreURI      string                 `json:\"StoreUri\"`\n\tAuth          string                 `json:\"Auth\"`\n\tUploadID      string                 `json:\"UploadID\"`\n\tUploadHeader  map[string]interface{} `json:\"UploadHeader,omitempty\"`\n\tStorageHeader map[string]interface{} `json:\"StorageHeader,omitempty\"`\n}\n\n// UploadAddress 上传地址信息\ntype UploadAddress struct {\n\tStoreInfos   []StoreInfo            `json:\"StoreInfos\"`\n\tUploadHosts  []string               `json:\"UploadHosts\"`\n\tUploadHeader map[string]interface{} `json:\"UploadHeader\"`\n\tSessionKey   string                 `json:\"SessionKey\"`\n\tCloud        string                 `json:\"Cloud\"`\n}\n\n// FallbackUploadAddress 备用上传地址\ntype FallbackUploadAddress struct {\n\tStoreInfos   []StoreInfo            `json:\"StoreInfos\"`\n\tUploadHosts  []string               `json:\"UploadHosts\"`\n\tUploadHeader map[string]interface{} `json:\"UploadHeader\"`\n\tSessionKey   string                 `json:\"SessionKey\"`\n\tCloud        string                 `json:\"Cloud\"`\n}\n\n// UploadNode 上传节点信息\ntype UploadNode struct {\n\tVid          string                 `json:\"Vid\"`\n\tVids         []string               `json:\"Vids\"`\n\tStoreInfos   []StoreInfo            `json:\"StoreInfos\"`\n\tUploadHost   string                 `json:\"UploadHost\"`\n\tUploadHeader map[string]interface{} `json:\"UploadHeader\"`\n\tType         string                 `json:\"Type\"`\n\tProtocol     string                 `json:\"Protocol\"`\n\tSessionKey   string                 `json:\"SessionKey\"`\n\tNodeConfig   struct {\n\t\tUploadMode string `json:\"UploadMode\"`\n\t} `json:\"NodeConfig\"`\n\tCluster string `json:\"Cluster\"`\n}\n\n// AdvanceOption 高级选项\ntype AdvanceOption struct {\n\tParallel      int    `json:\"Parallel\"`\n\tStream        int    `json:\"Stream\"`\n\tSliceSize     int    `json:\"SliceSize\"`\n\tEncryptionKey string `json:\"EncryptionKey\"`\n}\n\n// InnerUploadAddress 内部上传地址\ntype InnerUploadAddress struct {\n\tUploadNodes   []UploadNode  `json:\"UploadNodes\"`\n\tAdvanceOption AdvanceOption `json:\"AdvanceOption\"`\n}\n\n// UploadPart 上传分片信息\ntype UploadPart struct {\n\tUploadId   string `json:\"uploadid,omitempty\"`\n\tPartNumber string `json:\"part_number,omitempty\"`\n\tCrc32      string `json:\"crc32,omitempty\"`\n\tEtag       string `json:\"etag,omitempty\"`\n\tMode       string `json:\"mode,omitempty\"`\n}\n\n// UploadResp 上传响应体\ntype UploadResp struct {\n\tCode       int        `json:\"code\"`\n\tApiVersion string     `json:\"apiversion\"`\n\tMessage    string     `json:\"message\"`\n\tData       UploadPart `json:\"data\"`\n}\n\ntype VideoCommitUpload struct {\n\tVid       string `json:\"Vid\"`\n\tVideoMeta struct {\n\t\tURI          string  `json:\"Uri\"`\n\t\tHeight       int     `json:\"Height\"`\n\t\tWidth        int     `json:\"Width\"`\n\t\tOriginHeight int     `json:\"OriginHeight\"`\n\t\tOriginWidth  int     `json:\"OriginWidth\"`\n\t\tDuration     float64 `json:\"Duration\"`\n\t\tBitrate      int     `json:\"Bitrate\"`\n\t\tMd5          string  `json:\"Md5\"`\n\t\tFormat       string  `json:\"Format\"`\n\t\tSize         int     `json:\"Size\"`\n\t\tFileType     string  `json:\"FileType\"`\n\t\tCodec        string  `json:\"Codec\"`\n\t} `json:\"VideoMeta\"`\n\tWorkflowInput struct {\n\t\tTemplateID string `json:\"TemplateId\"`\n\t} `json:\"WorkflowInput\"`\n\tGetPosterMode string `json:\"GetPosterMode\"`\n}\n\ntype VideoCommitUploadResp struct {\n\tResponseMetadata ResponseMetadata `json:\"ResponseMetadata\"`\n\tResult           struct {\n\t\tRequestID string              `json:\"RequestId\"`\n\t\tResults   []VideoCommitUpload `json:\"Results\"`\n\t} `json:\"Result\"`\n}\n\ntype CommonResp struct {\n\tCode    int             `json:\"code\"`\n\tMsg     string          `json:\"msg,omitempty\"`\n\tMessage string          `json:\"message,omitempty\"` // 错误情况下的消息\n\tData    json.RawMessage `json:\"data,omitempty\"`    // 原始数据,稍后解析\n\tError   *struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tLocale  string `json:\"locale\"`\n\t} `json:\"error,omitempty\"`\n}\n\n// IsSuccess 判断响应是否成功\nfunc (r *CommonResp) IsSuccess() bool {\n\treturn r.Code == 0\n}\n\n// GetError 获取错误信息\nfunc (r *CommonResp) GetError() error {\n\tif r.IsSuccess() {\n\t\treturn nil\n\t}\n\t// 优先使用message字段\n\terrMsg := r.Message\n\tif errMsg == \"\" {\n\t\terrMsg = r.Msg\n\t}\n\t// 如果error对象存在且有详细消息,则使用error中的信息\n\tif r.Error != nil && r.Error.Message != \"\" {\n\t\terrMsg = r.Error.Message\n\t}\n\n\treturn fmt.Errorf(\"[doubao] API error (code: %d): %s\", r.Code, errMsg)\n}\n\n// UnmarshalData 将data字段解析为指定类型\nfunc (r *CommonResp) UnmarshalData(v interface{}) error {\n\tif !r.IsSuccess() {\n\t\treturn r.GetError()\n\t}\n\n\tif len(r.Data) == 0 {\n\t\treturn nil\n\t}\n\n\treturn json.Unmarshal(r.Data, v)\n}\n"
  },
  {
    "path": "drivers/doubao/util.go",
    "content": "package doubao\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tDirectoryType      = 1\n\tFileType           = 2\n\tLinkType           = 3\n\tImageType          = 4\n\tPagesType          = 5\n\tVideoType          = 6\n\tAudioType          = 7\n\tMeetingMinutesType = 8\n)\n\nvar FileNodeType = map[int]string{\n\t1: \"directory\",\n\t2: \"file\",\n\t3: \"link\",\n\t4: \"image\",\n\t5: \"pages\",\n\t6: \"video\",\n\t7: \"audio\",\n\t8: \"meeting_minutes\",\n}\n\nconst (\n\tBaseURL          = \"https://www.doubao.com\"\n\tFileDataType     = \"file\"\n\tImgDataType      = \"image\"\n\tVideoDataType    = \"video\"\n\tDefaultChunkSize = int64(5 * 1024 * 1024) // 5MB\n\tMaxRetryAttempts = 3                      // 最大重试次数\n\tUserAgent        = base.UserAgentNT\n\tRegion           = \"cn-north-1\"\n\tUploadTimeout    = 3 * time.Minute\n)\n\n// do others that not defined in Driver interface\nfunc (d *Doubao) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treqUrl := BaseURL + path\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Cookie\", d.Cookie)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tvar commonResp CommonResp\n\n\tres, err := req.Execute(method, reqUrl)\n\tlog.Debugln(res.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbody := res.Body()\n\t// 先解析为通用响应\n\tif err = json.Unmarshal(body, &commonResp); err != nil {\n\t\treturn nil, err\n\t}\n\t// 检查响应是否成功\n\tif !commonResp.IsSuccess() {\n\t\treturn body, commonResp.GetError()\n\t}\n\n\tif resp != nil {\n\t\tif err = json.Unmarshal(body, resp); err != nil {\n\t\t\treturn body, err\n\t\t}\n\t}\n\n\treturn body, nil\n}\n\nfunc (d *Doubao) getFiles(dirId, cursor string) (resp []File, err error) {\n\tvar r NodeInfoResp\n\n\tvar body = base.Json{\n\t\t\"node_id\": dirId,\n\t}\n\t// 如果有游标，则设置游标和大小\n\tif cursor != \"\" {\n\t\tbody[\"cursor\"] = cursor\n\t\tbody[\"size\"] = 50\n\t} else {\n\t\tbody[\"need_full_path\"] = false\n\t}\n\n\t_, err = d.request(\"/samantha/aispace/node_info\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif r.Data.Children != nil {\n\t\tresp = r.Data.Children\n\t}\n\n\tif r.Data.NextCursor != \"-1\" {\n\t\t// 递归获取下一页\n\t\tnextFiles, err := d.getFiles(dirId, r.Data.NextCursor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp = append(r.Data.Children, nextFiles...)\n\t}\n\n\treturn resp, err\n}\n\nfunc (d *Doubao) getUserInfo() (UserInfo, error) {\n\tvar r UserInfoResp\n\n\t_, err := d.request(\"/passport/account/info/v2/\", http.MethodGet, nil, &r)\n\tif err != nil {\n\t\treturn UserInfo{}, err\n\t}\n\n\treturn r.Data, err\n}\n\n// 签名请求\nfunc (d *Doubao) signRequest(req *resty.Request, method, tokenType, uploadUrl string) error {\n\tparsedUrl, err := url.Parse(uploadUrl)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid URL format: %w\", err)\n\t}\n\n\tvar accessKeyId, secretAccessKey, sessionToken string\n\tvar serviceName string\n\n\tif tokenType == VideoDataType {\n\t\taccessKeyId = d.UploadToken.Samantha.StsToken.AccessKeyID\n\t\tsecretAccessKey = d.UploadToken.Samantha.StsToken.SecretAccessKey\n\t\tsessionToken = d.UploadToken.Samantha.StsToken.SessionToken\n\t\tserviceName = \"vod\"\n\t} else {\n\t\taccessKeyId = d.UploadToken.Alice[tokenType].Auth.AccessKeyID\n\t\tsecretAccessKey = d.UploadToken.Alice[tokenType].Auth.SecretAccessKey\n\t\tsessionToken = d.UploadToken.Alice[tokenType].Auth.SessionToken\n\t\tserviceName = \"imagex\"\n\t}\n\n\t// 当前时间，格式为 ISO8601\n\tnow := time.Now().UTC()\n\tamzDate := now.Format(\"20060102T150405Z\")\n\tdateStamp := now.Format(\"20060102\")\n\n\treq.SetHeader(\"X-Amz-Date\", amzDate)\n\n\tif sessionToken != \"\" {\n\t\treq.SetHeader(\"X-Amz-Security-Token\", sessionToken)\n\t}\n\n\t// 计算请求体的SHA256哈希\n\tvar bodyHash string\n\tif req.Body != nil {\n\t\tbodyBytes, ok := req.Body.([]byte)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"request body must be []byte\")\n\t\t}\n\n\t\tbodyHash = hashSHA256(string(bodyBytes))\n\t\treq.SetHeader(\"X-Amz-Content-Sha256\", bodyHash)\n\t} else {\n\t\tbodyHash = hashSHA256(\"\")\n\t}\n\n\t// 创建规范请求\n\tcanonicalURI := parsedUrl.Path\n\tif canonicalURI == \"\" {\n\t\tcanonicalURI = \"/\"\n\t}\n\n\t// 查询参数按照字母顺序排序\n\tcanonicalQueryString := getCanonicalQueryString(req.QueryParam)\n\t// 规范请求头\n\tcanonicalHeaders, signedHeaders := getCanonicalHeadersFromMap(req.Header)\n\tcanonicalRequest := method + \"\\n\" +\n\t\tcanonicalURI + \"\\n\" +\n\t\tcanonicalQueryString + \"\\n\" +\n\t\tcanonicalHeaders + \"\\n\" +\n\t\tsignedHeaders + \"\\n\" +\n\t\tbodyHash\n\n\talgorithm := \"AWS4-HMAC-SHA256\"\n\tcredentialScope := fmt.Sprintf(\"%s/%s/%s/aws4_request\", dateStamp, Region, serviceName)\n\n\tstringToSign := algorithm + \"\\n\" +\n\t\tamzDate + \"\\n\" +\n\t\tcredentialScope + \"\\n\" +\n\t\thashSHA256(canonicalRequest)\n\t// 计算签名密钥\n\tsigningKey := getSigningKey(secretAccessKey, dateStamp, Region, serviceName)\n\t// 计算签名\n\tsignature := hmacSHA256Hex(signingKey, stringToSign)\n\t// 构建授权头\n\tauthorizationHeader := fmt.Sprintf(\n\t\t\"%s Credential=%s/%s, SignedHeaders=%s, Signature=%s\",\n\t\talgorithm,\n\t\taccessKeyId,\n\t\tcredentialScope,\n\t\tsignedHeaders,\n\t\tsignature,\n\t)\n\n\treq.SetHeader(\"Authorization\", authorizationHeader)\n\n\treturn nil\n}\n\nfunc (d *Doubao) requestApi(url, method, tokenType string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"user-agent\": UserAgent,\n\t})\n\n\tif method == http.MethodPost {\n\t\treq.SetHeader(\"Content-Type\", \"text/plain;charset=UTF-8\")\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\t// 使用自定义AWS SigV4签名\n\terr := d.signRequest(req, method, tokenType, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn res.Body(), nil\n}\n\nfunc (d *Doubao) initUploadToken() (*UploadToken, error) {\n\tuploadToken := &UploadToken{\n\t\tAlice:    make(map[string]UploadAuthToken),\n\t\tSamantha: MediaUploadAuthToken{},\n\t}\n\n\tfileAuthToken, err := d.getUploadAuthToken(FileDataType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\timgAuthToken, err := d.getUploadAuthToken(ImgDataType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmediaAuthToken, err := d.getSamantaUploadAuthToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuploadToken.Alice[FileDataType] = fileAuthToken\n\tuploadToken.Alice[ImgDataType] = imgAuthToken\n\tuploadToken.Samantha = mediaAuthToken\n\n\treturn uploadToken, nil\n}\n\nfunc (d *Doubao) getUploadAuthToken(dataType string) (ut UploadAuthToken, err error) {\n\tvar r UploadAuthTokenResp\n\t_, err = d.request(\"/alice/upload/auth_token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"scene\":     \"bot_chat\",\n\t\t\t\"data_type\": dataType,\n\t\t})\n\t}, &r)\n\n\treturn r.Data, err\n}\n\nfunc (d *Doubao) getSamantaUploadAuthToken() (mt MediaUploadAuthToken, err error) {\n\tvar r MediaUploadAuthTokenResp\n\t_, err = d.request(\"/samantha/media/get_upload_token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{})\n\t}, &r)\n\n\treturn r.Data, err\n}\n\n// getUploadConfig 获取上传配置信息\nfunc (d *Doubao) getUploadConfig(upConfig *UploadConfig, dataType string, file model.FileStreamer) error {\n\ttokenType := dataType\n\t// 配置参数函数\n\tconfigureParams := func() (string, map[string]string) {\n\t\tvar uploadUrl string\n\t\tvar params map[string]string\n\t\t// 根据数据类型设置不同的上传参数\n\t\tswitch dataType {\n\t\tcase VideoDataType:\n\t\t\t// 音频/视频类型 - 使用uploadToken.Samantha的配置\n\t\t\tuploadUrl = d.UploadToken.Samantha.UploadInfo.VideoHost\n\t\t\tparams = map[string]string{\n\t\t\t\t\"Action\":       \"ApplyUploadInner\",\n\t\t\t\t\"Version\":      \"2020-11-19\",\n\t\t\t\t\"SpaceName\":    d.UploadToken.Samantha.UploadInfo.SpaceName,\n\t\t\t\t\"FileType\":     \"video\",\n\t\t\t\t\"IsInner\":      \"1\",\n\t\t\t\t\"NeedFallback\": \"true\",\n\t\t\t\t\"FileSize\":     strconv.FormatInt(file.GetSize(), 10),\n\t\t\t\t\"s\":            randomString(),\n\t\t\t}\n\t\tcase ImgDataType, FileDataType:\n\t\t\t// 图片或其他文件类型 - 使用uploadToken.Alice对应配置\n\t\t\tuploadUrl = \"https://\" + d.UploadToken.Alice[dataType].UploadHost\n\t\t\tparams = map[string]string{\n\t\t\t\t\"Action\":        \"ApplyImageUpload\",\n\t\t\t\t\"Version\":       \"2018-08-01\",\n\t\t\t\t\"ServiceId\":     d.UploadToken.Alice[dataType].ServiceID,\n\t\t\t\t\"NeedFallback\":  \"true\",\n\t\t\t\t\"FileSize\":      strconv.FormatInt(file.GetSize(), 10),\n\t\t\t\t\"FileExtension\": stdpath.Ext(file.GetName()),\n\t\t\t\t\"s\":             randomString(),\n\t\t\t}\n\t\t}\n\t\treturn uploadUrl, params\n\t}\n\n\t// 获取初始参数\n\tuploadUrl, params := configureParams()\n\n\ttokenRefreshed := false\n\tvar configResp UploadConfigResp\n\n\terr := d._retryOperation(\"get upload_config\", func() error {\n\t\tconfigResp = UploadConfigResp{}\n\n\t\t_, err := d.requestApi(uploadUrl, http.MethodGet, tokenType, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(params)\n\t\t}, &configResp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif configResp.ResponseMetadata.Error.Code == \"\" {\n\t\t\t*upConfig = configResp.Result\n\t\t\treturn nil\n\t\t}\n\n\t\t// 100028 凭证过期\n\t\tif configResp.ResponseMetadata.Error.CodeN == 100028 && !tokenRefreshed {\n\t\t\tlog.Debugln(\"[doubao] Upload token expired, re-fetching...\")\n\t\t\tnewToken, err := d.initUploadToken()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %w\", err)\n\t\t\t}\n\n\t\t\td.UploadToken = newToken\n\t\t\ttokenRefreshed = true\n\t\t\tuploadUrl, params = configureParams()\n\n\t\t\treturn retry.Error{errors.New(\"token refreshed, retry needed\")}\n\t\t}\n\n\t\treturn fmt.Errorf(\"get upload_config failed: %s\", configResp.ResponseMetadata.Error.Message)\n\t})\n\n\treturn err\n}\n\n// uploadNode 上传 文件信息\nfunc (d *Doubao) uploadNode(uploadConfig *UploadConfig, dir model.Obj, file model.FileStreamer, dataType string) (UploadNodeResp, error) {\n\treqUuid := uuid.New().String()\n\tvar key string\n\tvar nodeType int\n\n\tmimetype := file.GetMimetype()\n\tswitch dataType {\n\tcase VideoDataType:\n\t\tkey = uploadConfig.InnerUploadAddress.UploadNodes[0].Vid\n\t\tif strings.HasPrefix(mimetype, \"audio/\") {\n\t\t\tnodeType = AudioType // 音频类型\n\t\t} else {\n\t\t\tnodeType = VideoType // 视频类型\n\t\t}\n\tcase ImgDataType:\n\t\tkey = uploadConfig.InnerUploadAddress.UploadNodes[0].StoreInfos[0].StoreURI\n\t\tnodeType = ImageType // 图片类型\n\tdefault: // FileDataType\n\t\tkey = uploadConfig.InnerUploadAddress.UploadNodes[0].StoreInfos[0].StoreURI\n\t\tnodeType = FileType // 文件类型\n\t}\n\n\tvar r UploadNodeResp\n\t_, err := d.request(\"/samantha/aispace/upload_node\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"node_list\": []base.Json{\n\t\t\t\t{\n\t\t\t\t\t\"local_id\":     reqUuid,\n\t\t\t\t\t\"parent_id\":    dir.GetID(),\n\t\t\t\t\t\"name\":         file.GetName(),\n\t\t\t\t\t\"key\":          key,\n\t\t\t\t\t\"node_content\": base.Json{},\n\t\t\t\t\t\"node_type\":    nodeType,\n\t\t\t\t\t\"size\":         file.GetSize(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"request_id\": reqUuid,\n\t\t})\n\t}, &r)\n\n\treturn r, err\n}\n\n// Upload 普通上传实现\nfunc (d *Doubao) Upload(ctx context.Context, config *UploadConfig, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, dataType string) (model.Obj, error) {\n\tss, err := stream.NewStreamSectionReader(file, int(file.GetSize()), &up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treader, err := ss.GetSectionReader(0, file.GetSize())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 计算CRC32\n\tcrc32Hash := crc32.NewIEEE()\n\tw, err := utils.CopyWithBuffer(crc32Hash, reader)\n\tif w != file.GetSize() {\n\t\treturn nil, fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", file.GetSize(), w, err)\n\t}\n\tcrc32Value := hex.EncodeToString(crc32Hash.Sum(nil))\n\n\t// 构建请求路径\n\tuploadNode := config.InnerUploadAddress.UploadNodes[0]\n\tstoreInfo := uploadNode.StoreInfos[0]\n\tuploadUrl := fmt.Sprintf(\"https://%s/upload/v1/%s\", uploadNode.UploadHost, storeInfo.StoreURI)\n\trateLimitedRd := driver.NewLimitedUploadStream(ctx, reader)\n\terr = d._retryOperation(\"Upload\", func() error {\n\t\treader.Seek(0, io.SeekStart)\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, rateLimitedRd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Header = map[string][]string{\n\t\t\t\"Referer\":             {BaseURL + \"/\"},\n\t\t\t\"Origin\":              {BaseURL},\n\t\t\t\"User-Agent\":          {UserAgent},\n\t\t\t\"X-Storage-U\":         {d.UserId},\n\t\t\t\"Authorization\":       {storeInfo.Auth},\n\t\t\t\"Content-Type\":        {\"application/octet-stream\"},\n\t\t\t\"Content-Crc32\":       {crc32Value},\n\t\t\t\"Content-Length\":      {strconv.FormatInt(file.GetSize(), 10)},\n\t\t\t\"Content-Disposition\": {fmt.Sprintf(\"attachment; filename=%s\", url.QueryEscape(storeInfo.StoreURI))},\n\t\t}\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer res.Body.Close()\n\t\tbytes, _ := io.ReadAll(res.Body)\n\t\tresp := UploadResp{}\n\t\tutils.Json.Unmarshal(bytes, &resp)\n\t\tif resp.Code != 2000 {\n\t\t\treturn fmt.Errorf(\"upload part failed: %s\", resp.Message)\n\t\t} else if resp.Data.Crc32 != crc32Value {\n\t\t\treturn fmt.Errorf(\"upload part failed: crc32 mismatch, expected %s, got %s\", crc32Value, resp.Data.Crc32)\n\t\t}\n\t\treturn nil\n\t})\n\tss.FreeSectionReader(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuploadNodeResp, err := d.uploadNode(config, dstDir, file, dataType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       uploadNodeResp.Data.NodeList[0].ID,\n\t\tName:     uploadNodeResp.Data.NodeList[0].Name,\n\t\tSize:     file.GetSize(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\n// UploadByMultipart 分片上传\nfunc (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fileSize int64, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, dataType string) (model.Obj, error) {\n\t// 构建请求路径\n\tuploadNode := config.InnerUploadAddress.UploadNodes[0]\n\tstoreInfo := uploadNode.StoreInfos[0]\n\tuploadUrl := fmt.Sprintf(\"https://%s/upload/v1/%s\", uploadNode.UploadHost, storeInfo.StoreURI)\n\t// 初始化分片上传\n\tvar uploadID string\n\terr := d._retryOperation(\"Initialize multipart upload\", func() error {\n\t\tvar err error\n\t\tuploadID, err = d.initMultipartUpload(config, uploadUrl, storeInfo)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize multipart upload: %w\", err)\n\t}\n\t// 准备分片参数\n\tchunkSize := DefaultChunkSize\n\tif config.InnerUploadAddress.AdvanceOption.SliceSize > 0 {\n\t\tchunkSize = int64(config.InnerUploadAddress.AdvanceOption.SliceSize)\n\t}\n\tss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttotalParts := (fileSize + chunkSize - 1) / chunkSize\n\t// 创建分片信息组\n\tparts := make([]UploadPart, totalParts)\n\n\tup(10.0) // 更新进度\n\t// 设置并行上传\n\tthread := min(int(totalParts), d.uploadThread)\n\tthreadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,\n\t\tretry.Attempts(MaxRetryAttempts),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay),\n\t\tretry.MaxJitter(200*time.Millisecond),\n\t)\n\n\t// 并行上传所有分片\n\tfor partIndex := range totalParts {\n\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\tbreak\n\t\t}\n\t\tpartNumber := partIndex + 1 // 分片编号从1开始\n\n\t\t// 计算此分片的大小和偏移\n\t\toffset := partIndex * chunkSize\n\t\tsize := chunkSize\n\t\tif partIndex == totalParts-1 {\n\t\t\tsize = fileSize - offset\n\t\t}\n\t\tvar reader io.ReadSeeker\n\t\tcrc32Value := \"\"\n\t\tthreadG.GoWithLifecycle(errgroup.Lifecycle{\n\t\t\tBefore: func(ctx context.Context) (err error) {\n\t\t\t\treader, err = ss.GetSectionReader(offset, size)\n\t\t\t\treturn\n\t\t\t},\n\t\t\tDo: func(ctx context.Context) (err error) {\n\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\tif crc32Value == \"\" {\n\t\t\t\t\t// 把耗时的计算放在这里，避免阻塞其他协程\n\t\t\t\t\tcrc32Hash := crc32.NewIEEE()\n\t\t\t\t\tw, err := utils.CopyWithBuffer(crc32Hash, reader)\n\t\t\t\t\tif w != size {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", size, w, err)\n\t\t\t\t\t}\n\t\t\t\t\tcrc32Value = hex.EncodeToString(crc32Hash.Sum(nil))\n\t\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\t}\n\t\t\t\treq, err := http.NewRequestWithContext(\n\t\t\t\t\tctx,\n\t\t\t\t\thttp.MethodPost,\n\t\t\t\t\tuploadUrl,\n\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, reader),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tquery := req.URL.Query()\n\t\t\t\tquery.Add(\"uploadid\", uploadID)\n\t\t\t\tquery.Add(\"part_number\", strconv.FormatInt(partNumber, 10))\n\t\t\t\tquery.Add(\"phase\", \"transfer\")\n\t\t\t\treq.URL.RawQuery = query.Encode()\n\t\t\t\treq.Header = map[string][]string{\n\t\t\t\t\t\"Referer\":             {BaseURL + \"/\"},\n\t\t\t\t\t\"Origin\":              {BaseURL},\n\t\t\t\t\t\"User-Agent\":          {UserAgent},\n\t\t\t\t\t\"X-Storage-U\":         {d.UserId},\n\t\t\t\t\t\"Authorization\":       {storeInfo.Auth},\n\t\t\t\t\t\"Content-Type\":        {\"application/octet-stream\"},\n\t\t\t\t\t\"Content-Crc32\":       {crc32Value},\n\t\t\t\t\t\"Content-Length\":      {strconv.FormatInt(size, 10)},\n\t\t\t\t\t\"Content-Disposition\": {fmt.Sprintf(\"attachment; filename=%s\", url.QueryEscape(storeInfo.StoreURI))},\n\t\t\t\t}\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\tbytes, _ := io.ReadAll(res.Body)\n\t\t\t\tuploadResp := UploadResp{}\n\t\t\t\tutils.Json.Unmarshal(bytes, &uploadResp)\n\t\t\t\tif uploadResp.Code != 2000 {\n\t\t\t\t\treturn fmt.Errorf(\"upload part failed: %s\", uploadResp.Message)\n\t\t\t\t} else if uploadResp.Data.Crc32 != crc32Value {\n\t\t\t\t\treturn fmt.Errorf(\"upload part failed: crc32 mismatch, expected %s, got %s\", crc32Value, uploadResp.Data.Crc32)\n\t\t\t\t}\n\t\t\t\t// 记录成功上传的分片\n\t\t\t\tparts[partIndex] = UploadPart{\n\t\t\t\t\tPartNumber: strconv.FormatInt(partNumber, 10),\n\t\t\t\t\tEtag:       uploadResp.Data.Etag,\n\t\t\t\t\tCrc32:      crc32Value,\n\t\t\t\t}\n\t\t\t\t// 更新进度\n\t\t\t\tprogress := 95 * float64(threadG.Success()+1) / float64(totalParts)\n\t\t\t\tup(progress)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tAfter: func(err error) {\n\t\t\t\tss.FreeSectionReader(reader)\n\t\t\t},\n\t\t})\n\t}\n\n\tif err = threadG.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\t// 完成上传-分片合并\n\tif err = d._retryOperation(\"Complete multipart upload\", func() error {\n\t\treturn d.completeMultipartUpload(config, uploadUrl, uploadID, parts)\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to complete multipart upload: %w\", err)\n\t}\n\t// 提交上传\n\tif err = d._retryOperation(\"Commit upload\", func() error {\n\t\treturn d.commitMultipartUpload(config)\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to commit upload: %w\", err)\n\t}\n\n\tup(98.0) // 更新到98%\n\t// 上传节点信息\n\tvar uploadNodeResp UploadNodeResp\n\n\tif err = d._retryOperation(\"Upload node\", func() error {\n\t\tvar err error\n\t\tuploadNodeResp, err = d.uploadNode(config, dstDir, file, dataType)\n\t\treturn err\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload node: %w\", err)\n\t}\n\n\tup(100.0) // 完成上传\n\n\treturn &model.Object{\n\t\tID:       uploadNodeResp.Data.NodeList[0].ID,\n\t\tName:     uploadNodeResp.Data.NodeList[0].Name,\n\t\tSize:     file.GetSize(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\n// 统一上传请求方法\nfunc (d *Doubao) uploadRequest(uploadUrl string, method string, storeInfo StoreInfo, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tclient := resty.New()\n\tclient.SetTransport(&http.Transport{\n\t\tDisableKeepAlives: true,  // 禁用连接复用\n\t\tForceAttemptHTTP2: false, // 强制使用HTTP/1.1\n\t})\n\tclient.SetTimeout(UploadTimeout)\n\n\treq := client.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Host\":          strings.Split(uploadUrl, \"/\")[2],\n\t\t\"Referer\":       BaseURL + \"/\",\n\t\t\"Origin\":        BaseURL,\n\t\t\"User-Agent\":    UserAgent,\n\t\t\"X-Storage-U\":   d.UserId,\n\t\t\"Authorization\": storeInfo.Auth,\n\t})\n\n\tif method == http.MethodPost {\n\t\treq.SetHeader(\"Content-Type\", \"text/plain;charset=UTF-8\")\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\tres, err := req.Execute(method, uploadUrl)\n\tif err != nil && err != io.EOF {\n\t\treturn nil, fmt.Errorf(\"upload request failed: %w\", err)\n\t}\n\n\treturn res.Body(), nil\n}\n\n// 初始化分片上传\nfunc (d *Doubao) initMultipartUpload(config *UploadConfig, uploadUrl string, storeInfo StoreInfo) (uploadId string, err error) {\n\tuploadResp := UploadResp{}\n\n\t_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"uploadmode\": \"part\",\n\t\t\t\"phase\":      \"init\",\n\t\t})\n\t}, &uploadResp)\n\n\tif err != nil {\n\t\treturn uploadId, err\n\t}\n\n\tif uploadResp.Code != 2000 {\n\t\treturn uploadId, fmt.Errorf(\"init upload failed: %s\", uploadResp.Message)\n\t}\n\n\treturn uploadResp.Data.UploadId, nil\n}\n\n// 完成分片上传\nfunc (d *Doubao) completeMultipartUpload(config *UploadConfig, uploadUrl, uploadID string, parts []UploadPart) error {\n\tuploadResp := UploadResp{}\n\n\tstoreInfo := config.InnerUploadAddress.UploadNodes[0].StoreInfos[0]\n\n\tbody := _convertUploadParts(parts)\n\n\terr := utils.Retry(MaxRetryAttempts, time.Second, func() (err error) {\n\t\t_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"uploadid\":   uploadID,\n\t\t\t\t\"phase\":      \"finish\",\n\t\t\t\t\"uploadmode\": \"part\",\n\t\t\t})\n\t\t\treq.SetBody(body)\n\t\t}, &uploadResp)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 检查响应状态码 2000 成功 4024 分片合并中\n\t\tif uploadResp.Code != 2000 && uploadResp.Code != 4024 {\n\t\t\treturn fmt.Errorf(\"finish upload failed: %s\", uploadResp.Message)\n\t\t}\n\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to complete multipart upload: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Doubao) commitMultipartUpload(uploadConfig *UploadConfig) error {\n\tuploadUrl := d.UploadToken.Samantha.UploadInfo.VideoHost\n\tparams := map[string]string{\n\t\t\"Action\":    \"CommitUploadInner\",\n\t\t\"Version\":   \"2020-11-19\",\n\t\t\"SpaceName\": d.UploadToken.Samantha.UploadInfo.SpaceName,\n\t}\n\ttokenType := VideoDataType\n\n\tvideoCommitUploadResp := VideoCommitUploadResp{}\n\n\tjsonBytes, err := json.Marshal(base.Json{\n\t\t\"SessionKey\": uploadConfig.InnerUploadAddress.UploadNodes[0].SessionKey,\n\t\t\"Functions\":  []base.Json{},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal request data: %w\", err)\n\t}\n\n\t_, err = d.requestApi(uploadUrl, http.MethodPost, tokenType, func(req *resty.Request) {\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetQueryParams(params)\n\t\treq.SetBody(jsonBytes)\n\n\t}, &videoCommitUploadResp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// _retryOperation 操作重试\nfunc (d *Doubao) _retryOperation(operation string, fn func() error) error {\n\treturn retry.Do(\n\t\tfn,\n\t\tretry.Attempts(MaxRetryAttempts),\n\t\tretry.Delay(500*time.Millisecond),\n\t\tretry.DelayType(retry.BackOffDelay),\n\t\tretry.MaxJitter(200*time.Millisecond),\n\t\tretry.OnRetry(func(n uint, err error) {\n\t\t\tlog.Debugf(\"[doubao] %s retry #%d: %v\", operation, n+1, err)\n\t\t}),\n\t)\n}\n\n// _convertUploadParts 将分片信息转换为字符串\nfunc _convertUploadParts(parts []UploadPart) string {\n\tif len(parts) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar result strings.Builder\n\n\tfor i, part := range parts {\n\t\tif i > 0 {\n\t\t\tresult.WriteString(\",\")\n\t\t}\n\t\tresult.WriteString(fmt.Sprintf(\"%s:%s\", part.PartNumber, part.Crc32))\n\t}\n\n\treturn result.String()\n}\n\n// 获取规范查询字符串\nfunc getCanonicalQueryString(query url.Values) string {\n\tif len(query) == 0 {\n\t\treturn \"\"\n\t}\n\n\tkeys := make([]string, 0, len(query))\n\tfor k := range query {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\n\tparts := make([]string, 0, len(keys))\n\tfor _, k := range keys {\n\t\tvalues := query[k]\n\t\tfor _, v := range values {\n\t\t\tparts = append(parts, urlEncode(k)+\"=\"+urlEncode(v))\n\t\t}\n\t}\n\n\treturn strings.Join(parts, \"&\")\n}\n\nfunc urlEncode(s string) string {\n\ts = url.QueryEscape(s)\n\ts = strings.ReplaceAll(s, \"+\", \"%20\")\n\treturn s\n}\n\n// 获取规范头信息和已签名头列表\nfunc getCanonicalHeadersFromMap(headers map[string][]string) (string, string) {\n\t// 不可签名的头部列表\n\tunsignableHeaders := map[string]bool{\n\t\t\"authorization\":     true,\n\t\t\"content-type\":      true,\n\t\t\"content-length\":    true,\n\t\t\"user-agent\":        true,\n\t\t\"presigned-expires\": true,\n\t\t\"expect\":            true,\n\t\t\"x-amzn-trace-id\":   true,\n\t}\n\theaderValues := make(map[string]string)\n\tvar signedHeadersList []string\n\n\tfor k, v := range headers {\n\t\tif len(v) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tlowerKey := strings.ToLower(k)\n\t\t// 检查是否可签名\n\t\tif strings.HasPrefix(lowerKey, \"x-amz-\") || !unsignableHeaders[lowerKey] {\n\t\t\tvalue := strings.TrimSpace(v[0])\n\t\t\tvalue = strings.Join(strings.Fields(value), \" \")\n\t\t\theaderValues[lowerKey] = value\n\t\t\tsignedHeadersList = append(signedHeadersList, lowerKey)\n\t\t}\n\t}\n\n\tsort.Strings(signedHeadersList)\n\n\tvar canonicalHeadersStr strings.Builder\n\tfor _, key := range signedHeadersList {\n\t\tcanonicalHeadersStr.WriteString(key)\n\t\tcanonicalHeadersStr.WriteString(\":\")\n\t\tcanonicalHeadersStr.WriteString(headerValues[key])\n\t\tcanonicalHeadersStr.WriteString(\"\\n\")\n\t}\n\n\tsignedHeaders := strings.Join(signedHeadersList, \";\")\n\n\treturn canonicalHeadersStr.String(), signedHeaders\n}\n\n// 计算HMAC-SHA256\nfunc hmacSHA256(key []byte, data string) []byte {\n\th := hmac.New(sha256.New, key)\n\th.Write([]byte(data))\n\treturn h.Sum(nil)\n}\n\n// 计算HMAC-SHA256并返回十六进制字符串\nfunc hmacSHA256Hex(key []byte, data string) string {\n\treturn hex.EncodeToString(hmacSHA256(key, data))\n}\n\n// 计算SHA256哈希并返回十六进制字符串\nfunc hashSHA256(data string) string {\n\th := sha256.New()\n\th.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// 获取签名密钥\nfunc getSigningKey(secretKey, dateStamp, region, service string) []byte {\n\tkDate := hmacSHA256([]byte(\"AWS4\"+secretKey), dateStamp)\n\tkRegion := hmacSHA256(kDate, region)\n\tkService := hmacSHA256(kRegion, service)\n\tkSigning := hmacSHA256(kService, \"aws4_request\")\n\treturn kSigning\n}\n\nfunc randomString() string {\n\tconst charset = \"0123456789abcdefghijklmnopqrstuvwxyz\"\n\tconst length = 11 // 11位随机字符串\n\n\tvar sb strings.Builder\n\tsb.Grow(length)\n\n\tfor i := 0; i < length; i++ {\n\t\tsb.WriteByte(charset[rand.Intn(len(charset))])\n\t}\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "drivers/doubao_share/driver.go",
    "content": "package doubao_share\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype DoubaoShare struct {\n\tmodel.Storage\n\tAddition\n\tRootFiles []RootFileList\n}\n\nfunc (d *DoubaoShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *DoubaoShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *DoubaoShare) Init(ctx context.Context) error {\n\t// 初始化 虚拟分享列表\n\tif err := d.initShareList(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *DoubaoShare) Drop(ctx context.Context) error {\n\treturn nil\n}\n\n// 潜在bug：配置二级目录时，可能会出问题\nfunc (d *DoubaoShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\t// 检查是否为根目录\n\tif dir.GetID() == \"\" && dir.GetPath() == \"/\" {\n\t\treturn d.listRootDirectory(ctx)\n\t}\n\n\t// 非根目录，处理不同情况\n\tif fo, ok := dir.(*FileObject); ok {\n\t\tif fo.ShareID == \"\" {\n\t\t\t// 虚拟目录，需要列出子目录\n\t\t\treturn d.listVirtualDirectoryContent(dir)\n\t\t} else {\n\t\t\t// 具有分享ID的目录，获取此分享下的文件\n\t\t\tshareId, relativePath, err := d._findShareAndPath(dir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)\n\t\t}\n\t}\n\n\t// 使用通用方法\n\tshareId, relativePath, err := d._findShareAndPath(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 获取指定路径下的文件\n\treturn d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)\n}\n\nfunc (d *DoubaoShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar downloadUrl string\n\n\tif u, ok := file.(*FileObject); ok {\n\t\tswitch u.NodeType {\n\t\tcase VideoType, AudioType:\n\t\t\tvar r GetVideoFileUrlResp\n\t\t\t_, err := d.request(\"/samantha/media/get_play_info\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\"key\":      u.Key,\n\t\t\t\t\t\"share_id\": u.ShareID,\n\t\t\t\t\t\"node_id\":  file.GetID(),\n\t\t\t\t})\n\t\t\t}, &r)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdownloadUrl = r.Data.OriginalMediaInfo.MainURL\n\t\tdefault:\n\t\t\tvar r GetDownloadInfoResp\n\t\t\t_, err := d.request(\"/samantha/aispace/get_download_info\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\"requests\": []base.Json{{\"node_id\": file.GetID()}},\n\t\t\t\t})\n\t\t\t}, &r)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdownloadUrl = r.Data.DownloadInfos[0].MainURL\n\t\t}\n\n\t\t// 生成标准的Content-Disposition\n\t\tcontentDisposition := utils.GenerateContentDisposition(u.Name)\n\n\t\treturn &model.Link{\n\t\t\tURL: downloadUrl,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"User-Agent\":          []string{UserAgent},\n\t\t\t\t\"Content-Disposition\": []string{contentDisposition},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\treturn nil, errors.New(\"can't convert obj to URL\")\n}\n\nfunc (d *DoubaoShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\t// TODO create folder, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO move obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\t// TODO rename obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO copy obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// TODO upload file, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *DoubaoShare) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n\t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n\t// return errs.NotImplement to use an internal archive tool\n\treturn nil, errs.NotImplement\n}\n\n//func (d *DoubaoShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*DoubaoShare)(nil)\n"
  },
  {
    "path": "drivers/doubao_share/meta.go",
    "content": "package doubao_share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tCookie   string `json:\"cookie\" type:\"text\"`\n\tShareIds string `json:\"share_ids\" type:\"text\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:        \"DoubaoShare\",\n\tLocalSort:   true,\n\tNoUpload:    true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &DoubaoShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/doubao_share/types.go",
    "content": "package doubao_share\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype BaseResp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n}\n\ntype NodeInfoData struct {\n\tShare      ShareInfo   `json:\"share,omitempty\"`\n\tCreator    CreatorInfo `json:\"creator,omitempty\"`\n\tNodeList   []File      `json:\"node_list,omitempty\"`\n\tNodeInfo   File        `json:\"node_info,omitempty\"`\n\tChildren   []File      `json:\"children,omitempty\"`\n\tPath       FilePath    `json:\"path,omitempty\"`\n\tNextCursor string      `json:\"next_cursor,omitempty\"`\n\tHasMore    bool        `json:\"has_more,omitempty\"`\n}\n\ntype NodeInfoResp struct {\n\tBaseResp\n\tNodeInfoData `json:\"data\"`\n}\n\ntype RootFileList struct {\n\tShareID     string\n\tVirtualPath string\n\tNodeInfo    NodeInfoData\n\tChild       *[]RootFileList\n}\n\ntype File struct {\n\tID                  string `json:\"id\"`\n\tName                string `json:\"name\"`\n\tKey                 string `json:\"key\"`\n\tNodeType            int    `json:\"node_type\"`\n\tSize                int64  `json:\"size\"`\n\tSource              int    `json:\"source\"`\n\tNameReviewStatus    int    `json:\"name_review_status\"`\n\tContentReviewStatus int    `json:\"content_review_status\"`\n\tRiskReviewStatus    int    `json:\"risk_review_status\"`\n\tConversationID      string `json:\"conversation_id\"`\n\tParentID            string `json:\"parent_id\"`\n\tCreateTime          int64  `json:\"create_time\"`\n\tUpdateTime          int64  `json:\"update_time\"`\n}\n\ntype FileObject struct {\n\tmodel.Object\n\tShareID  string\n\tKey      string\n\tNodeID   string\n\tNodeType int\n}\n\ntype ShareInfo struct {\n\tShareID   string `json:\"share_id\"`\n\tFirstNode struct {\n\t\tID       string `json:\"id\"`\n\t\tName     string `json:\"name\"`\n\t\tKey      string `json:\"key\"`\n\t\tNodeType int    `json:\"node_type\"`\n\t\tSize     int    `json:\"size\"`\n\t\tSource   int    `json:\"source\"`\n\t\tContent  struct {\n\t\t\tLinkFileType  string `json:\"link_file_type\"`\n\t\t\tImageWidth    int    `json:\"image_width\"`\n\t\t\tImageHeight   int    `json:\"image_height\"`\n\t\t\tAiSkillStatus int    `json:\"ai_skill_status\"`\n\t\t} `json:\"content\"`\n\t\tNameReviewStatus    int    `json:\"name_review_status\"`\n\t\tContentReviewStatus int    `json:\"content_review_status\"`\n\t\tRiskReviewStatus    int    `json:\"risk_review_status\"`\n\t\tConversationID      string `json:\"conversation_id\"`\n\t\tParentID            string `json:\"parent_id\"`\n\t\tCreateTime          int64  `json:\"create_time\"`\n\t\tUpdateTime          int64  `json:\"update_time\"`\n\t} `json:\"first_node\"`\n\tNodeCount      int    `json:\"node_count\"`\n\tCreateTime     int64  `json:\"create_time\"`\n\tChannel        string `json:\"channel\"`\n\tInfluencerType int    `json:\"influencer_type\"`\n}\n\ntype CreatorInfo struct {\n\tEntityID string `json:\"entity_id\"`\n\tUserName string `json:\"user_name\"`\n\tNickName string `json:\"nick_name\"`\n\tAvatar   struct {\n\t\tOriginURL string `json:\"origin_url\"`\n\t\tTinyURL   string `json:\"tiny_url\"`\n\t\tURI       string `json:\"uri\"`\n\t} `json:\"avatar\"`\n}\n\ntype FilePath []struct {\n\tID                  string `json:\"id\"`\n\tName                string `json:\"name\"`\n\tKey                 string `json:\"key\"`\n\tNodeType            int    `json:\"node_type\"`\n\tSize                int    `json:\"size\"`\n\tSource              int    `json:\"source\"`\n\tNameReviewStatus    int    `json:\"name_review_status\"`\n\tContentReviewStatus int    `json:\"content_review_status\"`\n\tRiskReviewStatus    int    `json:\"risk_review_status\"`\n\tConversationID      string `json:\"conversation_id\"`\n\tParentID            string `json:\"parent_id\"`\n\tCreateTime          int64  `json:\"create_time\"`\n\tUpdateTime          int64  `json:\"update_time\"`\n}\n\ntype GetDownloadInfoResp struct {\n\tBaseResp\n\tData struct {\n\t\tDownloadInfos []struct {\n\t\t\tNodeID    string `json:\"node_id\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"download_infos\"`\n\t} `json:\"data\"`\n}\n\ntype GetVideoFileUrlResp struct {\n\tBaseResp\n\tData struct {\n\t\tMediaType string `json:\"media_type\"`\n\t\tMediaInfo []struct {\n\t\t\tMeta struct {\n\t\t\t\tHeight     string  `json:\"height\"`\n\t\t\t\tWidth      string  `json:\"width\"`\n\t\t\t\tFormat     string  `json:\"format\"`\n\t\t\t\tDuration   float64 `json:\"duration\"`\n\t\t\t\tCodecType  string  `json:\"codec_type\"`\n\t\t\t\tDefinition string  `json:\"definition\"`\n\t\t\t} `json:\"meta\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"media_info\"`\n\t\tOriginalMediaInfo struct {\n\t\t\tMeta struct {\n\t\t\t\tHeight     string  `json:\"height\"`\n\t\t\t\tWidth      string  `json:\"width\"`\n\t\t\t\tFormat     string  `json:\"format\"`\n\t\t\t\tDuration   float64 `json:\"duration\"`\n\t\t\t\tCodecType  string  `json:\"codec_type\"`\n\t\t\t\tDefinition string  `json:\"definition\"`\n\t\t\t} `json:\"meta\"`\n\t\t\tMainURL   string `json:\"main_url\"`\n\t\t\tBackupURL string `json:\"backup_url\"`\n\t\t} `json:\"original_media_info\"`\n\t\tPosterURL      string `json:\"poster_url\"`\n\t\tPlayableStatus int    `json:\"playable_status\"`\n\t} `json:\"data\"`\n}\n\ntype CommonResp struct {\n\tCode    int             `json:\"code\"`\n\tMsg     string          `json:\"msg,omitempty\"`\n\tMessage string          `json:\"message,omitempty\"` // 错误情况下的消息\n\tData    json.RawMessage `json:\"data,omitempty\"`    // 原始数据,稍后解析\n\tError   *struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tLocale  string `json:\"locale\"`\n\t} `json:\"error,omitempty\"`\n}\n\n// IsSuccess 判断响应是否成功\nfunc (r *CommonResp) IsSuccess() bool {\n\treturn r.Code == 0\n}\n\n// GetError 获取错误信息\nfunc (r *CommonResp) GetError() error {\n\tif r.IsSuccess() {\n\t\treturn nil\n\t}\n\t// 优先使用message字段\n\terrMsg := r.Message\n\tif errMsg == \"\" {\n\t\terrMsg = r.Msg\n\t}\n\t// 如果error对象存在且有详细消息,则使用error中的信息\n\tif r.Error != nil && r.Error.Message != \"\" {\n\t\terrMsg = r.Error.Message\n\t}\n\n\treturn fmt.Errorf(\"[doubao] API error (code: %d): %s\", r.Code, errMsg)\n}\n\n// UnmarshalData 将data字段解析为指定类型\nfunc (r *CommonResp) UnmarshalData(v interface{}) error {\n\tif !r.IsSuccess() {\n\t\treturn r.GetError()\n\t}\n\n\tif len(r.Data) == 0 {\n\t\treturn nil\n\t}\n\n\treturn json.Unmarshal(r.Data, v)\n}\n"
  },
  {
    "path": "drivers/doubao_share/util.go",
    "content": "package doubao_share\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tDirectoryType      = 1\n\tFileType           = 2\n\tLinkType           = 3\n\tImageType          = 4\n\tPagesType          = 5\n\tVideoType          = 6\n\tAudioType          = 7\n\tMeetingMinutesType = 8\n)\n\nvar FileNodeType = map[int]string{\n\t1: \"directory\",\n\t2: \"file\",\n\t3: \"link\",\n\t4: \"image\",\n\t5: \"pages\",\n\t6: \"video\",\n\t7: \"audio\",\n\t8: \"meeting_minutes\",\n}\n\nconst (\n\tBaseURL       = \"https://www.doubao.com\"\n\tFileDataType  = \"file\"\n\tImgDataType   = \"image\"\n\tVideoDataType = \"video\"\n\tUserAgent     = base.UserAgentNT\n)\n\nfunc (d *DoubaoShare) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treqUrl := BaseURL + path\n\treq := base.RestyClient.R()\n\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":     d.Cookie,\n\t\t\"User-Agent\": UserAgent,\n\t})\n\n\treq.SetQueryParams(map[string]string{\n\t\t\"version_code\":    \"20800\",\n\t\t\"device_platform\": \"web\",\n\t})\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tvar commonResp CommonResp\n\n\tres, err := req.Execute(method, reqUrl)\n\tlog.Debugln(res.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbody := res.Body()\n\t// 先解析为通用响应\n\tif err = json.Unmarshal(body, &commonResp); err != nil {\n\t\treturn nil, err\n\t}\n\t// 检查响应是否成功\n\tif !commonResp.IsSuccess() {\n\t\treturn body, commonResp.GetError()\n\t}\n\n\tif resp != nil {\n\t\tif err = json.Unmarshal(body, resp); err != nil {\n\t\t\treturn body, err\n\t\t}\n\t}\n\n\treturn body, nil\n}\n\nfunc (d *DoubaoShare) getFiles(dirId, nodeId, cursor string) (resp []File, err error) {\n\tvar r NodeInfoResp\n\n\tvar body = base.Json{\n\t\t\"share_id\": dirId,\n\t\t\"node_id\":  nodeId,\n\t}\n\t// 如果有游标，则设置游标和大小\n\tif cursor != \"\" {\n\t\tbody[\"cursor\"] = cursor\n\t\tbody[\"size\"] = 50\n\t} else {\n\t\tbody[\"need_full_path\"] = false\n\t}\n\n\t_, err = d.request(\"/samantha/aispace/share/node_info\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif r.NodeInfoData.Children != nil {\n\t\tresp = r.NodeInfoData.Children\n\t}\n\n\tif r.NodeInfoData.NextCursor != \"-1\" {\n\t\t// 递归获取下一页\n\t\tnextFiles, err := d.getFiles(dirId, nodeId, r.NodeInfoData.NextCursor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp = append(r.NodeInfoData.Children, nextFiles...)\n\t}\n\n\treturn resp, err\n}\n\nfunc (d *DoubaoShare) getShareOverview(shareId, cursor string) (resp []File, err error) {\n\treturn d.getShareOverviewWithHistory(shareId, cursor, make(map[string]bool))\n}\n\nfunc (d *DoubaoShare) getShareOverviewWithHistory(shareId, cursor string, cursorHistory map[string]bool) (resp []File, err error) {\n\tvar r NodeInfoResp\n\n\tvar body = base.Json{\n\t\t\"share_id\": shareId,\n\t}\n\t// 如果有游标，则设置游标和大小\n\tif cursor != \"\" {\n\t\tbody[\"cursor\"] = cursor\n\t\tbody[\"size\"] = 50\n\t} else {\n\t\tbody[\"need_full_path\"] = false\n\t}\n\n\t_, err = d.request(\"/samantha/aispace/share/overview\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif r.NodeInfoData.NodeList != nil {\n\t\tresp = r.NodeInfoData.NodeList\n\t}\n\n\tif r.NodeInfoData.NextCursor != \"-1\" {\n\t\t// 检查游标是否重复出现，防止无限循环\n\t\tif cursorHistory[r.NodeInfoData.NextCursor] {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\t// 记录当前游标\n\t\tcursorHistory[r.NodeInfoData.NextCursor] = true\n\n\t\t// 递归获取下一页\n\t\tnextFiles, err := d.getShareOverviewWithHistory(shareId, r.NodeInfoData.NextCursor, cursorHistory)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp = append(resp, nextFiles...)\n\t}\n\n\treturn resp, nil\n}\n\nfunc (d *DoubaoShare) initShareList() error {\n\tif d.Addition.ShareIds == \"\" {\n\t\treturn fmt.Errorf(\"share_ids is empty\")\n\t}\n\n\t// 解析分享配置\n\tshareConfigs, rootShares, err := d._parseShareConfigs()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 检查路径冲突\n\tif err := d._detectPathConflicts(shareConfigs); err != nil {\n\t\treturn err\n\t}\n\n\t// 构建树形结构\n\trootMap := d._buildTreeStructure(shareConfigs, rootShares)\n\n\t// 提取顶级节点\n\ttopLevelNodes := d._extractTopLevelNodes(rootMap, rootShares)\n\tif len(topLevelNodes) == 0 {\n\t\treturn fmt.Errorf(\"no valid share_ids found\")\n\t}\n\n\t// 存储结果\n\td.RootFiles = topLevelNodes\n\n\treturn nil\n}\n\n// 从配置中解析分享ID和路径\nfunc (d *DoubaoShare) _parseShareConfigs() (map[string]string, []string, error) {\n\tshareConfigs := make(map[string]string) // 路径 -> 分享ID\n\trootShares := make([]string, 0)         // 根目录显示的分享ID\n\n\tlines := strings.Split(strings.TrimSpace(d.Addition.ShareIds), \"\\n\")\n\tif len(lines) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"no share_ids found\")\n\t}\n\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 解析分享ID和路径\n\t\tparts := strings.Split(line, \"|\")\n\t\tvar shareId, sharePath string\n\n\t\tif len(parts) == 1 {\n\t\t\t// 无路径分享，直接在根目录显示\n\t\t\tshareId = _extractShareId(parts[0])\n\t\t\tif shareId != \"\" {\n\t\t\t\trootShares = append(rootShares, shareId)\n\t\t\t}\n\t\t\tcontinue\n\t\t} else if len(parts) >= 2 {\n\t\t\tshareId = _extractShareId(parts[0])\n\t\t\tsharePath = strings.Trim(parts[1], \"/\")\n\t\t}\n\n\t\tif shareId == \"\" {\n\t\t\tlog.Warnf(\"[doubao_share] Invalid Share_id Format: %s\", line)\n\t\t\tcontinue\n\t\t}\n\n\t\t// 空路径也加入根目录显示\n\t\tif sharePath == \"\" {\n\t\t\trootShares = append(rootShares, shareId)\n\t\t\tcontinue\n\t\t}\n\n\t\t// 添加到路径映射\n\t\tshareConfigs[sharePath] = shareId\n\t}\n\n\treturn shareConfigs, rootShares, nil\n}\n\n// 检测路径冲突\nfunc (d *DoubaoShare) _detectPathConflicts(shareConfigs map[string]string) error {\n\t// 检查直接路径冲突\n\tpathToShareIds := make(map[string][]string)\n\tfor sharePath, id := range shareConfigs {\n\t\tpathToShareIds[sharePath] = append(pathToShareIds[sharePath], id)\n\t}\n\n\tfor sharePath, ids := range pathToShareIds {\n\t\tif len(ids) > 1 {\n\t\t\treturn fmt.Errorf(\"路径冲突: 路径 '%s' 被多个不同的分享ID使用: %s\",\n\t\t\t\tsharePath, strings.Join(ids, \", \"))\n\t\t}\n\t}\n\n\t// 检查层次冲突\n\tfor path1, id1 := range shareConfigs {\n\t\tfor path2, id2 := range shareConfigs {\n\t\t\tif path1 == path2 || id1 == id2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 检查前缀冲突\n\t\t\tif strings.HasPrefix(path2, path1+\"/\") || strings.HasPrefix(path1, path2+\"/\") {\n\t\t\t\treturn fmt.Errorf(\"路径冲突: 路径 '%s' (ID: %s) 与路径 '%s' (ID: %s) 存在层次冲突\",\n\t\t\t\t\tpath1, id1, path2, id2)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// 构建树形结构\nfunc (d *DoubaoShare) _buildTreeStructure(shareConfigs map[string]string, rootShares []string) map[string]*RootFileList {\n\trootMap := make(map[string]*RootFileList)\n\n\t// 添加所有分享节点\n\tfor sharePath, shareId := range shareConfigs {\n\t\tchildren := make([]RootFileList, 0)\n\t\trootMap[sharePath] = &RootFileList{\n\t\t\tShareID:     shareId,\n\t\t\tVirtualPath: sharePath,\n\t\t\tNodeInfo:    NodeInfoData{},\n\t\t\tChild:       &children,\n\t\t}\n\t}\n\n\t// 构建父子关系\n\tfor sharePath, node := range rootMap {\n\t\tif sharePath == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpathParts := strings.Split(sharePath, \"/\")\n\t\tif len(pathParts) > 1 {\n\t\t\tparentPath := strings.Join(pathParts[:len(pathParts)-1], \"/\")\n\n\t\t\t// 确保所有父级路径都已创建\n\t\t\t_ensurePathExists(rootMap, parentPath)\n\n\t\t\t// 添加当前节点到父节点\n\t\t\tif parent, exists := rootMap[parentPath]; exists {\n\t\t\t\t*parent.Child = append(*parent.Child, *node)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn rootMap\n}\n\n// 提取顶级节点\nfunc (d *DoubaoShare) _extractTopLevelNodes(rootMap map[string]*RootFileList, rootShares []string) []RootFileList {\n\tvar topLevelNodes []RootFileList\n\n\t// 添加根目录分享\n\tfor _, shareId := range rootShares {\n\t\tchildren := make([]RootFileList, 0)\n\t\ttopLevelNodes = append(topLevelNodes, RootFileList{\n\t\t\tShareID:     shareId,\n\t\t\tVirtualPath: \"\",\n\t\t\tNodeInfo:    NodeInfoData{},\n\t\t\tChild:       &children,\n\t\t})\n\t}\n\n\t// 添加顶级目录\n\tfor rootPath, node := range rootMap {\n\t\tif rootPath == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tisTopLevel := true\n\t\tpathParts := strings.Split(rootPath, \"/\")\n\n\t\tif len(pathParts) > 1 {\n\t\t\tparentPath := strings.Join(pathParts[:len(pathParts)-1], \"/\")\n\t\t\tif _, exists := rootMap[parentPath]; exists {\n\t\t\t\tisTopLevel = false\n\t\t\t}\n\t\t}\n\n\t\tif isTopLevel {\n\t\t\ttopLevelNodes = append(topLevelNodes, *node)\n\t\t}\n\t}\n\n\treturn topLevelNodes\n}\n\n// 确保路径存在，创建所有必要的中间节点\nfunc _ensurePathExists(rootMap map[string]*RootFileList, path string) {\n\tif path == \"\" {\n\t\treturn\n\t}\n\n\t// 如果路径已存在，不需要再处理\n\tif _, exists := rootMap[path]; exists {\n\t\treturn\n\t}\n\n\t// 创建当前路径节点\n\tchildren := make([]RootFileList, 0)\n\trootMap[path] = &RootFileList{\n\t\tShareID:     \"\",\n\t\tVirtualPath: path,\n\t\tNodeInfo:    NodeInfoData{},\n\t\tChild:       &children,\n\t}\n\n\t// 处理父路径\n\tpathParts := strings.Split(path, \"/\")\n\tif len(pathParts) > 1 {\n\t\tparentPath := strings.Join(pathParts[:len(pathParts)-1], \"/\")\n\n\t\t// 确保父路径存在\n\t\t_ensurePathExists(rootMap, parentPath)\n\n\t\t// 将当前节点添加为父节点的子节点\n\t\tif parent, exists := rootMap[parentPath]; exists {\n\t\t\t*parent.Child = append(*parent.Child, *rootMap[path])\n\t\t}\n\t}\n}\n\n// _extractShareId 从URL或直接ID中提取分享ID\nfunc _extractShareId(input string) string {\n\tinput = strings.TrimSpace(input)\n\tif strings.HasPrefix(input, \"http\") {\n\t\tregex := regexp.MustCompile(`/drive/s/([a-zA-Z0-9]+)`)\n\t\tif matches := regex.FindStringSubmatch(input); len(matches) > 1 {\n\t\t\treturn matches[1]\n\t\t}\n\t\treturn \"\"\n\t}\n\treturn input // 直接返回ID\n}\n\n// _findRootFileByShareID 查找指定ShareID的配置\nfunc _findRootFileByShareID(rootFiles []RootFileList, shareID string) *RootFileList {\n\tfor i, rf := range rootFiles {\n\t\tif rf.ShareID == shareID {\n\t\t\treturn &rootFiles[i]\n\t\t}\n\t\tif rf.Child != nil && len(*rf.Child) > 0 {\n\t\t\tif found := _findRootFileByShareID(*rf.Child, shareID); found != nil {\n\t\t\t\treturn found\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// _findNodeByPath 查找指定路径的节点\nfunc _findNodeByPath(rootFiles []RootFileList, path string) *RootFileList {\n\tfor i, rf := range rootFiles {\n\t\tif rf.VirtualPath == path {\n\t\t\treturn &rootFiles[i]\n\t\t}\n\t\tif rf.Child != nil && len(*rf.Child) > 0 {\n\t\t\tif found := _findNodeByPath(*rf.Child, path); found != nil {\n\t\t\t\treturn found\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// _findShareByPath 根据路径查找分享和相对路径\nfunc _findShareByPath(rootFiles []RootFileList, path string) (*RootFileList, string) {\n\t// 完全匹配或子路径匹配\n\tfor i, rf := range rootFiles {\n\t\tif rf.VirtualPath == path {\n\t\t\treturn &rootFiles[i], \"\"\n\t\t}\n\n\t\tif rf.VirtualPath != \"\" && strings.HasPrefix(path, rf.VirtualPath+\"/\") {\n\t\t\trelPath := strings.TrimPrefix(path, rf.VirtualPath+\"/\")\n\n\t\t\t// 先检查子节点\n\t\t\tif rf.Child != nil && len(*rf.Child) > 0 {\n\t\t\t\tif child, childPath := _findShareByPath(*rf.Child, path); child != nil {\n\t\t\t\t\treturn child, childPath\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn &rootFiles[i], relPath\n\t\t}\n\n\t\t// 递归检查子节点\n\t\tif rf.Child != nil && len(*rf.Child) > 0 {\n\t\t\tif child, childPath := _findShareByPath(*rf.Child, path); child != nil {\n\t\t\t\treturn child, childPath\n\t\t\t}\n\t\t}\n\t}\n\n\t// 检查根目录分享\n\tfor i, rf := range rootFiles {\n\t\tif rf.VirtualPath == \"\" && rf.ShareID != \"\" {\n\t\t\tparts := strings.SplitN(path, \"/\", 2)\n\t\t\tif len(parts) > 0 && parts[0] == rf.ShareID {\n\t\t\t\tif len(parts) > 1 {\n\t\t\t\t\treturn &rootFiles[i], parts[1]\n\t\t\t\t}\n\t\t\t\treturn &rootFiles[i], \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, \"\"\n}\n\n// _findShareAndPath 根据给定路径查找对应的ShareID和相对路径\nfunc (d *DoubaoShare) _findShareAndPath(dir model.Obj) (string, string, error) {\n\tdirPath := dir.GetPath()\n\n\t// 如果是根目录，返回空值表示需要列出所有分享\n\tif dirPath == \"/\" || dirPath == \"\" {\n\t\treturn \"\", \"\", nil\n\t}\n\n\t// 检查是否是 FileObject 类型，并获取 ShareID\n\tif fo, ok := dir.(*FileObject); ok && fo.ShareID != \"\" {\n\t\t// 直接使用对象中存储的 ShareID\n\t\t// 计算相对路径（移除前导斜杠）\n\t\trelativePath := strings.TrimPrefix(dirPath, \"/\")\n\n\t\t// 递归查找对应的 RootFile\n\t\tfound := _findRootFileByShareID(d.RootFiles, fo.ShareID)\n\t\tif found != nil {\n\t\t\tif found.VirtualPath != \"\" {\n\t\t\t\t// 如果此分享配置了路径前缀，需要考虑相对路径的计算\n\t\t\t\tif strings.HasPrefix(relativePath, found.VirtualPath) {\n\t\t\t\t\treturn fo.ShareID, strings.TrimPrefix(relativePath, found.VirtualPath+\"/\"), nil\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fo.ShareID, relativePath, nil\n\t\t}\n\n\t\t// 如果找不到对应的 RootFile 配置，仍然使用对象中的 ShareID\n\t\treturn fo.ShareID, relativePath, nil\n\t}\n\n\t// 移除开头的斜杠\n\tcleanPath := strings.TrimPrefix(dirPath, \"/\")\n\n\t// 先检查是否有直接匹配的根目录分享\n\tfor _, rootFile := range d.RootFiles {\n\t\tif rootFile.VirtualPath == \"\" && rootFile.ShareID != \"\" {\n\t\t\t// 检查是否匹配当前路径的第一部分\n\t\t\tparts := strings.SplitN(cleanPath, \"/\", 2)\n\t\t\tif len(parts) > 0 && parts[0] == rootFile.ShareID {\n\t\t\t\tif len(parts) > 1 {\n\t\t\t\t\treturn rootFile.ShareID, parts[1], nil\n\t\t\t\t}\n\t\t\t\treturn rootFile.ShareID, \"\", nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// 查找匹配此路径的分享或虚拟目录\n\tshare, relPath := _findShareByPath(d.RootFiles, cleanPath)\n\tif share != nil {\n\t\treturn share.ShareID, relPath, nil\n\t}\n\n\tlog.Warnf(\"[doubao_share] No matching share path found: %s\", dirPath)\n\treturn \"\", \"\", fmt.Errorf(\"no matching share path found: %s\", dirPath)\n}\n\n// convertToFileObject 将File转换为FileObject\nfunc (d *DoubaoShare) convertToFileObject(file File, shareId string, relativePath string) *FileObject {\n\t// 构建文件对象\n\tobj := &FileObject{\n\t\tObject: model.Object{\n\t\t\tID:       file.ID,\n\t\t\tName:     file.Name,\n\t\t\tSize:     file.Size,\n\t\t\tModified: time.Unix(file.UpdateTime, 0),\n\t\t\tCtime:    time.Unix(file.CreateTime, 0),\n\t\t\tIsFolder: file.NodeType == DirectoryType,\n\t\t\tPath:     path.Join(relativePath, file.Name),\n\t\t},\n\t\tShareID:  shareId,\n\t\tKey:      file.Key,\n\t\tNodeID:   file.ID,\n\t\tNodeType: file.NodeType,\n\t}\n\n\treturn obj\n}\n\n// getFilesInPath 获取指定分享和路径下的文件\nfunc (d *DoubaoShare) getFilesInPath(ctx context.Context, shareId, nodeId, relativePath string) ([]model.Obj, error) {\n\tvar (\n\t\tfiles []File\n\t\terr   error\n\t)\n\n\t// 调用overview接口获取分享链接信息 nodeId\n\tif nodeId == \"\" {\n\t\tfiles, err = d.getShareOverview(shareId, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get share link information: %w\", err)\n\t\t}\n\n\t\tresult := make([]model.Obj, 0, len(files))\n\t\tfor _, file := range files {\n\t\t\tresult = append(result, d.convertToFileObject(file, shareId, \"/\"))\n\t\t}\n\n\t\treturn result, nil\n\n\t} else {\n\t\tfiles, err = d.getFiles(shareId, nodeId, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get share file: %w\", err)\n\t\t}\n\n\t\tresult := make([]model.Obj, 0, len(files))\n\t\tfor _, file := range files {\n\t\t\tresult = append(result, d.convertToFileObject(file, shareId, path.Join(\"/\", relativePath)))\n\t\t}\n\n\t\treturn result, nil\n\t}\n}\n\n// listRootDirectory 处理根目录的内容展示\nfunc (d *DoubaoShare) listRootDirectory(ctx context.Context) ([]model.Obj, error) {\n\tobjects := make([]model.Obj, 0)\n\n\t// 分组处理：直接显示的分享内容 vs 虚拟目录\n\tvar directShareIDs []string\n\taddedDirs := make(map[string]bool)\n\n\t// 处理所有根节点\n\tfor _, rootFile := range d.RootFiles {\n\t\tif rootFile.VirtualPath == \"\" && rootFile.ShareID != \"\" {\n\t\t\t// 无路径分享，记录ShareID以便后续获取内容\n\t\t\tdirectShareIDs = append(directShareIDs, rootFile.ShareID)\n\t\t} else {\n\t\t\t// 有路径的分享，显示第一级目录\n\t\t\tparts := strings.SplitN(rootFile.VirtualPath, \"/\", 2)\n\t\t\tfirstLevel := parts[0]\n\n\t\t\t// 避免重复添加同名目录\n\t\t\tif _, exists := addedDirs[firstLevel]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 创建虚拟目录对象\n\t\t\tobj := &FileObject{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       \"\",\n\t\t\t\t\tName:     firstLevel,\n\t\t\t\t\tModified: time.Now(),\n\t\t\t\t\tCtime:    time.Now(),\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t\tPath:     path.Join(\"/\", firstLevel),\n\t\t\t\t},\n\t\t\t\tShareID:  rootFile.ShareID,\n\t\t\t\tKey:      \"\",\n\t\t\t\tNodeID:   \"\",\n\t\t\t\tNodeType: DirectoryType,\n\t\t\t}\n\t\t\tobjects = append(objects, obj)\n\t\t\taddedDirs[firstLevel] = true\n\t\t}\n\t}\n\n\t// 处理直接显示的分享内容\n\tfor _, shareID := range directShareIDs {\n\t\tshareFiles, err := d.getFilesInPath(ctx, shareID, \"\", \"\")\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"[doubao_share] Failed to get list of files in share %s: %s\", shareID, err)\n\t\t\tcontinue\n\t\t}\n\t\tobjects = append(objects, shareFiles...)\n\t}\n\n\treturn objects, nil\n}\n\n// listVirtualDirectoryContent 列出虚拟目录的内容\nfunc (d *DoubaoShare) listVirtualDirectoryContent(dir model.Obj) ([]model.Obj, error) {\n\tdirPath := strings.TrimPrefix(dir.GetPath(), \"/\")\n\tobjects := make([]model.Obj, 0)\n\n\t// 递归查找此路径的节点\n\tnode := _findNodeByPath(d.RootFiles, dirPath)\n\n\tif node != nil && node.Child != nil {\n\t\t// 显示此节点的所有子节点\n\t\tfor _, child := range *node.Child {\n\t\t\t// 计算显示名称（取路径的最后一部分）\n\t\t\tdisplayName := child.VirtualPath\n\t\t\tif child.VirtualPath != \"\" {\n\t\t\t\tparts := strings.Split(child.VirtualPath, \"/\")\n\t\t\t\tdisplayName = parts[len(parts)-1]\n\t\t\t} else if child.ShareID != \"\" {\n\t\t\t\tdisplayName = child.ShareID\n\t\t\t}\n\n\t\t\tobj := &FileObject{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       \"\",\n\t\t\t\t\tName:     displayName,\n\t\t\t\t\tModified: time.Now(),\n\t\t\t\t\tCtime:    time.Now(),\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t\tPath:     path.Join(\"/\", child.VirtualPath),\n\t\t\t\t},\n\t\t\t\tShareID:  child.ShareID,\n\t\t\t\tKey:      \"\",\n\t\t\t\tNodeID:   \"\",\n\t\t\t\tNodeType: DirectoryType,\n\t\t\t}\n\t\t\tobjects = append(objects, obj)\n\t\t}\n\t}\n\n\treturn objects, nil\n}\n"
  },
  {
    "path": "drivers/dropbox/driver.go",
    "content": "package dropbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Dropbox struct {\n\tmodel.Storage\n\tAddition\n\tbase        string\n\tcontentBase string\n}\n\nfunc (d *Dropbox) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Dropbox) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Dropbox) Init(ctx context.Context) error {\n\tquery := \"foo\"\n\tres, err := d.request(\"/2/check/user\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"query\": query,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tresult := utils.Json.Get(res, \"result\").ToString()\n\tif result != query {\n\t\treturn fmt.Errorf(\"failed to check user: %s\", string(res))\n\t}\n\td.RootNamespaceId, err = d.GetRootNamespaceId(ctx)\n\n\treturn err\n}\n\nfunc (d *Dropbox) GetRootNamespaceId(ctx context.Context) (string, error) {\n\tres, err := d.request(\"/2/users/get_current_account\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(nil)\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar currentAccountResp CurrentAccountResp\n\terr = utils.Json.Unmarshal(res, &currentAccountResp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\trootNamespaceId := currentAccountResp.RootInfo.RootNamespaceId\n\treturn rootNamespaceId, nil\n}\n\nfunc (d *Dropbox) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Dropbox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *Dropbox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tres, err := d.request(\"/2/files/get_temporary_link\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"path\": file.GetPath(),\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\turl := utils.Json.Get(res, \"link\").ToString()\n\texp := time.Hour\n\treturn &model.Link{\n\t\tURL:        url,\n\t\tExpiration: &exp,\n\t}, nil\n}\n\nfunc (d *Dropbox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := d.request(\"/2/files/create_folder_v2\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"autorename\": false,\n\t\t\t\"path\":       parentDir.GetPath() + \"/\" + dirName,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Dropbox) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\ttoPath := dstDir.GetPath() + \"/\" + srcObj.GetName()\n\n\t_, err := d.request(\"/2/files/move_v2\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"allow_ownership_transfer\": false,\n\t\t\t\"allow_shared_folder\":      false,\n\t\t\t\"autorename\":               false,\n\t\t\t\"from_path\":                srcObj.GetID(),\n\t\t\t\"to_path\":                  toPath,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Dropbox) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tpath := srcObj.GetPath()\n\tfileName := srcObj.GetName()\n\ttoPath := path[:len(path)-len(fileName)] + newName\n\n\t_, err := d.request(\"/2/files/move_v2\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"allow_ownership_transfer\": false,\n\t\t\t\"allow_shared_folder\":      false,\n\t\t\t\"autorename\":               false,\n\t\t\t\"from_path\":                srcObj.GetID(),\n\t\t\t\"to_path\":                  toPath,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Dropbox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\ttoPath := dstDir.GetPath() + \"/\" + srcObj.GetName()\n\t_, err := d.request(\"/2/files/copy_v2\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"allow_ownership_transfer\": false,\n\t\t\t\"allow_shared_folder\":      false,\n\t\t\t\"autorename\":               false,\n\t\t\t\"from_path\":                srcObj.GetID(),\n\t\t\t\"to_path\":                  toPath,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Dropbox) Remove(ctx context.Context, obj model.Obj) error {\n\turi := \"/2/files/delete_v2\"\n\t_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"path\": obj.GetID(),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Dropbox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\t// 1. start\n\tsessionId, err := d.startUploadSession(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 2.append\n\t// A single request should not upload more than 150 MB, and each call must be multiple of 4MB  (except for last call)\n\tconst PartSize = 20971520\n\tcount := 1\n\tif stream.GetSize() > PartSize {\n\t\tcount = int(math.Ceil(float64(stream.GetSize()) / float64(PartSize)))\n\t}\n\toffset := int64(0)\n\n\tfor i := 0; i < count; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\n\t\tstart := i * PartSize\n\t\tbyteSize := stream.GetSize() - int64(start)\n\t\tif byteSize > PartSize {\n\t\t\tbyteSize = PartSize\n\t\t}\n\n\t\turl := d.contentBase + \"/2/files/upload_session/append_v2\"\n\t\treader := driver.NewLimitedUploadStream(ctx, io.LimitReader(stream, PartSize))\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, reader)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to update file when append to upload session, err: %+v\", err)\n\t\t\treturn err\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+d.AccessToken)\n\n\t\targs := UploadAppendArgs{\n\t\t\tClose: false,\n\t\t\tCursor: UploadCursor{\n\t\t\t\tOffset:    offset,\n\t\t\t\tSessionID: sessionId,\n\t\t\t},\n\t\t}\n\t\targsJson, err := utils.Json.MarshalToString(args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Header.Set(\"Dropbox-API-Arg\", argsJson)\n\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_ = res.Body.Close()\n\t\tup(float64(i+1) * 100 / float64(count))\n\t\toffset += byteSize\n\t}\n\t// 3.finish\n\ttoPath := dstDir.GetPath() + \"/\" + stream.GetName()\n\terr2 := d.finishUploadSession(ctx, toPath, offset, sessionId)\n\tif err2 != nil {\n\t\treturn err2\n\t}\n\n\treturn err\n}\n\nvar _ driver.Driver = (*Dropbox)(nil)\n"
  },
  {
    "path": "drivers/dropbox/meta.go",
    "content": "package dropbox\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tUseOnlineAPI    bool   `json:\"use_online_api\" default:\"false\"`\n\tAPIAddress      string `json:\"api_url_address\" default:\"https://api.oplist.org/dropboxs/renewapi\"`\n\tClientID        string `json:\"client_id\" required:\"false\" help:\"Keep it empty if you don't have one\"`\n\tClientSecret    string `json:\"client_secret\" required:\"false\" help:\"Keep it empty if you don't have one\"`\n\tAccessToken     string\n\tRefreshToken    string `json:\"refresh_token\" required:\"true\"`\n\tRootNamespaceId string `json:\"RootNamespaceId\" required:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Dropbox\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Dropbox{\n\t\t\tbase:        \"https://api.dropboxapi.com\",\n\t\t\tcontentBase: \"https://content.dropboxapi.com\",\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/dropbox/types.go",
    "content": "package dropbox\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype TokenResp struct {\n\tAccessToken string `json:\"access_token\"`\n\tTokenType   string `json:\"token_type\"`\n\tExpiresIn   int    `json:\"expires_in\"`\n}\n\ntype ErrorResp struct {\n\tError struct {\n\t\tTag string `json:\".tag\"`\n\t} `json:\"error\"`\n\tErrorSummary string `json:\"error_summary\"`\n}\n\ntype RefreshTokenErrorResp struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\ntype CurrentAccountResp struct {\n\tRootInfo struct {\n\t\tRootNamespaceId string `json:\"root_namespace_id\"`\n\t\tHomeNamespaceId string `json:\"home_namespace_id\"`\n\t} `json:\"root_info\"`\n}\n\ntype File struct {\n\tTag            string    `json:\".tag\"`\n\tName           string    `json:\"name\"`\n\tPathLower      string    `json:\"path_lower\"`\n\tPathDisplay    string    `json:\"path_display\"`\n\tID             string    `json:\"id\"`\n\tClientModified time.Time `json:\"client_modified\"`\n\tServerModified time.Time `json:\"server_modified\"`\n\tRev            string    `json:\"rev\"`\n\tSize           int       `json:\"size\"`\n\tIsDownloadable bool      `json:\"is_downloadable\"`\n\tContentHash    string    `json:\"content_hash\"`\n}\n\ntype ListResp struct {\n\tEntries []File `json:\"entries\"`\n\tCursor  string `json:\"cursor\"`\n\tHasMore bool   `json:\"has_more\"`\n}\n\ntype UploadCursor struct {\n\tOffset    int64  `json:\"offset\"`\n\tSessionID string `json:\"session_id\"`\n}\n\ntype UploadAppendArgs struct {\n\tClose  bool         `json:\"close\"`\n\tCursor UploadCursor `json:\"cursor\"`\n}\n\ntype UploadFinishArgs struct {\n\tCommit struct {\n\t\tAutorename     bool   `json:\"autorename\"`\n\t\tMode           string `json:\"mode\"`\n\t\tMute           bool   `json:\"mute\"`\n\t\tPath           string `json:\"path\"`\n\t\tStrictConflict bool   `json:\"strict_conflict\"`\n\t} `json:\"commit\"`\n\tCursor UploadCursor `json:\"cursor\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.ID,\n\t\t\tPath:     f.PathDisplay,\n\t\t\tName:     f.Name,\n\t\t\tSize:     int64(f.Size),\n\t\t\tModified: f.ServerModified,\n\t\t\tIsFolder: f.Tag == \"folder\",\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}\n}\n"
  },
  {
    "path": "drivers/dropbox/util.go",
    "content": "package dropbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *Dropbox) refreshToken() error {\n\t// 使用在线API刷新Token，无需ClientID和ClientSecret\n\tif d.UseOnlineAPI && len(d.APIAddress) > 0 {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"dropboxs_go\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\td.RefreshToken = resp.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\turl := d.base + \"/oauth2/token\"\n\tvar tokenResp TokenResp\n\tresp, err := base.RestyClient.R().\n\t\t//ForceContentType(\"application/x-www-form-urlencoded\").\n\t\t//SetBasicAuth(d.ClientID, d.ClientSecret).\n\t\tSetFormData(map[string]string{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t\t\"client_id\":     d.ClientID,\n\t\t\t\"client_secret\": d.ClientSecret,\n\t\t}).\n\t\tPost(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"[dropbox] refresh token response: %s\", resp.String())\n\tif resp.StatusCode() != 200 {\n\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.String())\n\t}\n\t_ = utils.Json.UnmarshalFromString(resp.String(), &tokenResp)\n\td.AccessToken = tokenResp.AccessToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif d.RootNamespaceId != \"\" {\n\t\tapiPathRootJson, err := utils.Json.MarshalToString(map[string]interface{}{\n\t\t\t\".tag\": \"root\",\n\t\t\t\"root\": d.RootNamespaceId,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treq.SetHeader(\"Dropbox-API-Path-Root\", apiPathRootJson)\n\t}\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif method == http.MethodPost && req.Body != nil {\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t}\n\tvar e ErrorResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, d.base+uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"[dropbox] request (%s) response: %s\", uri, res.String())\n\tisRetry := len(retry) > 0 && retry[0]\n\tif res.StatusCode() != 200 {\n\t\tbody := res.String()\n\t\tif !isRetry && (utils.SliceMeet([]string{\"expired_access_token\", \"invalid_access_token\", \"authorization\"}, body,\n\t\t\tfunc(item string, v string) bool {\n\t\t\t\treturn strings.Contains(v, item)\n\t\t\t}) || d.AccessToken == \"\") {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(uri, method, callback, true)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%s:%s\", e.Error, e.ErrorSummary)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Dropbox) list(ctx context.Context, data base.Json, isContinue bool) (*ListResp, error) {\n\tvar resp ListResp\n\turi := \"/2/files/list_folder\"\n\tif isContinue {\n\t\turi += \"/continue\"\n\t}\n\t_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(data).SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Dropbox) getFiles(ctx context.Context, path string) ([]File, error) {\n\thasMore := true\n\tvar marker string\n\tres := make([]File, 0)\n\n\tdata := base.Json{\n\t\t\"include_deleted\":                     false,\n\t\t\"include_has_explicit_shared_members\": false,\n\t\t\"include_mounted_folders\":             false,\n\t\t\"include_non_downloadable_files\":      false,\n\t\t\"limit\":                               2000,\n\t\t\"path\":                                path,\n\t\t\"recursive\":                           false,\n\t}\n\tresp, err := d.list(ctx, data, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmarker = resp.Cursor\n\thasMore = resp.HasMore\n\tres = append(res, resp.Entries...)\n\n\tfor hasMore {\n\t\tdata := base.Json{\n\t\t\t\"cursor\": marker,\n\t\t}\n\t\tresp, err := d.list(ctx, data, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmarker = resp.Cursor\n\t\thasMore = resp.HasMore\n\t\tres = append(res, resp.Entries...)\n\t}\n\treturn res, nil\n}\n\nfunc (d *Dropbox) finishUploadSession(ctx context.Context, toPath string, offset int64, sessionId string) error {\n\turl := d.contentBase + \"/2/files/upload_session/finish\"\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif d.RootNamespaceId != \"\" {\n\t\tapiPathRootJson, err := d.buildPathRootHeader()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Header.Set(\"Dropbox-API-Path-Root\", apiPathRootJson)\n\t}\n\n\tuploadFinishArgs := UploadFinishArgs{\n\t\tCommit: struct {\n\t\t\tAutorename     bool   `json:\"autorename\"`\n\t\t\tMode           string `json:\"mode\"`\n\t\t\tMute           bool   `json:\"mute\"`\n\t\t\tPath           string `json:\"path\"`\n\t\t\tStrictConflict bool   `json:\"strict_conflict\"`\n\t\t}{\n\t\t\tAutorename:     true,\n\t\t\tMode:           \"add\",\n\t\t\tMute:           false,\n\t\t\tPath:           toPath,\n\t\t\tStrictConflict: false,\n\t\t},\n\t\tCursor: UploadCursor{\n\t\t\tOffset:    offset,\n\t\t\tSessionID: sessionId,\n\t\t},\n\t}\n\n\targsJson, err := utils.Json.MarshalToString(uploadFinishArgs)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Dropbox-API-Arg\", argsJson)\n\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to update file when finish session, err: %+v\", err)\n\t\treturn err\n\t}\n\t_ = res.Body.Close()\n\treturn nil\n}\n\nfunc (d *Dropbox) startUploadSession(ctx context.Context) (string, error) {\n\turl := d.contentBase + \"/2/files/upload_session/start\"\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif d.RootNamespaceId != \"\" {\n\t\tapiPathRootJson, err := d.buildPathRootHeader()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treq.Header.Set(\"Dropbox-API-Path-Root\", apiPathRootJson)\n\t}\n\treq.Header.Set(\"Dropbox-API-Arg\", \"{\\\"close\\\":false}\")\n\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to update file when start session, err: %+v\", err)\n\t\treturn \"\", err\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tsessionId := utils.Json.Get(body, \"session_id\").ToString()\n\n\t_ = res.Body.Close()\n\treturn sessionId, nil\n}\n\nfunc (d *Dropbox) buildPathRootHeader() (string, error) {\n\treturn utils.Json.MarshalToString(map[string]interface{}{\n\t\t\".tag\": \"root\",\n\t\t\"root\": d.RootNamespaceId,\n\t})\n}\n"
  },
  {
    "path": "drivers/febbox/driver.go",
    "content": "package febbox\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype FebBox struct {\n\tmodel.Storage\n\tAddition\n\taccessToken string\n\toauth2Token oauth2.TokenSource\n}\n\nfunc (d *FebBox) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *FebBox) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *FebBox) Init(ctx context.Context) error {\n\t// 初始化 oauth2Config\n\toauth2Config := &clientcredentials.Config{\n\t\tClientID:     d.ClientID,\n\t\tClientSecret: d.ClientSecret,\n\t\tAuthStyle:    oauth2.AuthStyleInParams,\n\t\tTokenURL:     \"https://api.febbox.com/oauth/token\",\n\t}\n\n\td.initializeOAuth2Token(ctx, oauth2Config, d.Addition.RefreshToken)\n\n\ttoken, err := d.oauth2Token.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.accessToken = token.AccessToken\n\td.Addition.RefreshToken = token.RefreshToken\n\top.MustSaveDriverStorage(d)\n\n\treturn nil\n}\n\nfunc (d *FebBox) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *FebBox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFilesList(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *FebBox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar ip string\n\tif d.Addition.UserIP != \"\" {\n\t\tip = d.Addition.UserIP\n\t} else {\n\t\tip = args.IP\n\t}\n\n\turl, err := d.getDownloadLink(file.GetID(), ip)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: url,\n\t}, nil\n}\n\nfunc (d *FebBox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\terr := d.makeDir(parentDir.GetID(), dirName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *FebBox) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\terr := d.move(srcObj.GetID(), dstDir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *FebBox) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\terr := d.rename(srcObj.GetID(), newName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *FebBox) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\terr := d.copy(srcObj.GetID(), dstDir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *FebBox) Remove(ctx context.Context, obj model.Obj) error {\n\terr := d.remove(obj.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *FebBox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nvar _ driver.Driver = (*FebBox)(nil)\n"
  },
  {
    "path": "drivers/febbox/meta.go",
    "content": "package febbox\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tClientID     string `json:\"client_id\" required:\"true\" default:\"\"`\n\tClientSecret string `json:\"client_secret\" required:\"true\" default:\"\"`\n\tRefreshToken string\n\tSortRule     string `json:\"sort_rule\" required:\"true\" type:\"select\" options:\"size_asc,size_desc,name_asc,name_desc,update_asc,update_desc,ext_asc,ext_desc\" default:\"name_asc\"`\n\tPageSize     int64  `json:\"page_size\" required:\"true\" type:\"number\" default:\"100\" help:\"list api per page size of FebBox driver\"`\n\tUserIP       string `json:\"user_ip\" default:\"\" help:\"user ip address for download link which can speed up the download\"`\n}\n\nvar config = driver.Config{\n\tName:          \"FebBox\",\n\tNoUpload:      true,\n\tDefaultRoot:   \"0\",\n\tLinkCacheMode: driver.LinkCacheIP,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &FebBox{}\n\t})\n}\n"
  },
  {
    "path": "drivers/febbox/oauth2.go",
    "content": "package febbox\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n)\n\ntype customTokenSource struct {\n\tconfig       *clientcredentials.Config\n\tctx          context.Context\n\trefreshToken string\n}\n\nfunc (c *customTokenSource) Token() (*oauth2.Token, error) {\n\tv := url.Values{}\n\tif c.refreshToken != \"\" {\n\t\tv.Set(\"grant_type\", \"refresh_token\")\n\t\tv.Set(\"refresh_token\", c.refreshToken)\n\t} else {\n\t\tv.Set(\"grant_type\", \"client_credentials\")\n\t}\n\n\tv.Set(\"client_id\", c.config.ClientID)\n\tv.Set(\"client_secret\", c.config.ClientSecret)\n\n\treq, err := http.NewRequestWithContext(c.ctx, http.MethodPost, c.config.TokenURL, strings.NewReader(v.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.New(\"oauth2: cannot fetch token\")\n\t}\n\n\tvar tokenResp struct {\n\t\tCode int    `json:\"code\"`\n\t\tMsg  string `json:\"msg\"`\n\t\tData struct {\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tExpiresIn    int64  `json:\"expires_in\"`\n\t\t\tTokenType    string `json:\"token_type\"`\n\t\t\tScope        string `json:\"scope\"`\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t} `json:\"data\"`\n\t}\n\n\tif err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif tokenResp.Code != 1 {\n\t\treturn nil, errors.New(\"oauth2: server response error\")\n\t}\n\n\tc.refreshToken = tokenResp.Data.RefreshToken\n\n\ttoken := &oauth2.Token{\n\t\tAccessToken:  tokenResp.Data.AccessToken,\n\t\tTokenType:    tokenResp.Data.TokenType,\n\t\tRefreshToken: tokenResp.Data.RefreshToken,\n\t\tExpiry:       time.Now().Add(time.Duration(tokenResp.Data.ExpiresIn) * time.Second),\n\t}\n\n\treturn token, nil\n}\n\nfunc (d *FebBox) initializeOAuth2Token(ctx context.Context, oauth2Config *clientcredentials.Config, refreshToken string) {\n\td.oauth2Token = oauth2.ReuseTokenSource(nil, &customTokenSource{\n\t\tconfig:       oauth2Config,\n\t\tctx:          ctx,\n\t\trefreshToken: refreshToken,\n\t})\n}\n"
  },
  {
    "path": "drivers/febbox/types.go",
    "content": "package febbox\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n)\n\ntype ErrResp struct {\n\tErrorCode     int64   `json:\"code\"`\n\tErrorMsg      string  `json:\"msg\"`\n\tServerRunTime float64 `json:\"server_runtime\"`\n\tServerName    string  `json:\"server_name\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ServerRunTime != 0 || e.ServerName != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ServerRunTime: %f ,ServerName: %s\", e.ErrorCode, e.ErrorMsg, e.ServerRunTime, e.ServerName)\n}\n\ntype FileListResp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\tData struct {\n\t\tFileList []File `json:\"file_list\"`\n\t\tShowType string `json:\"show_type\"`\n\t} `json:\"data\"`\n}\n\ntype Rules struct {\n\tAllowCopy     int64 `json:\"allow_copy\"`\n\tAllowDelete   int64 `json:\"allow_delete\"`\n\tAllowDownload int64 `json:\"allow_download\"`\n\tAllowComment  int64 `json:\"allow_comment\"`\n\tHideLocation  int64 `json:\"hide_location\"`\n}\n\ntype File struct {\n\tFid              int64  `json:\"fid\"`\n\tUID              int64  `json:\"uid\"`\n\tFileSize         int64  `json:\"file_size\"`\n\tPath             string `json:\"path\"`\n\tFileName         string `json:\"file_name\"`\n\tExt              string `json:\"ext\"`\n\tAddTime          int64  `json:\"add_time\"`\n\tFileCreateTime   int64  `json:\"file_create_time\"`\n\tFileUpdateTime   int64  `json:\"file_update_time\"`\n\tParentID         int64  `json:\"parent_id\"`\n\tUpdateTime       int64  `json:\"update_time\"`\n\tLastOpenTime     int64  `json:\"last_open_time\"`\n\tIsDir            int64  `json:\"is_dir\"`\n\tEpub             int64  `json:\"epub\"`\n\tIsMusicList      int64  `json:\"is_music_list\"`\n\tOssFid           int64  `json:\"oss_fid\"`\n\tFaststart        int64  `json:\"faststart\"`\n\tHasVideoQuality  int64  `json:\"has_video_quality\"`\n\tTotalDownload    int64  `json:\"total_download\"`\n\tStatus           int64  `json:\"status\"`\n\tRemark           string `json:\"remark\"`\n\tOldHash          string `json:\"old_hash\"`\n\tHash             string `json:\"hash\"`\n\tHashType         string `json:\"hash_type\"`\n\tFromUID          int64  `json:\"from_uid\"`\n\tFidOrg           int64  `json:\"fid_org\"`\n\tShareID          int64  `json:\"share_id\"`\n\tInvitePermission int64  `json:\"invite_permission\"`\n\tThumbSmall       string `json:\"thumb_small\"`\n\tThumbSmallWidth  int64  `json:\"thumb_small_width\"`\n\tThumbSmallHeight int64  `json:\"thumb_small_height\"`\n\tThumb            string `json:\"thumb\"`\n\tThumbWidth       int64  `json:\"thumb_width\"`\n\tThumbHeight      int64  `json:\"thumb_height\"`\n\tThumbBig         string `json:\"thumb_big\"`\n\tThumbBigWidth    int64  `json:\"thumb_big_width\"`\n\tThumbBigHeight   int64  `json:\"thumb_big_height\"`\n\tIsCustomThumb    int64  `json:\"is_custom_thumb\"`\n\tPhotos           int64  `json:\"photos\"`\n\tIsAlbum          int64  `json:\"is_album\"`\n\tReadOnly         int64  `json:\"read_only\"`\n\tRules            Rules  `json:\"rules\"`\n\tIsShared         int64  `json:\"is_shared\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       strconv.FormatInt(f.Fid, 10),\n\t\t\tName:     f.FileName,\n\t\t\tSize:     f.FileSize,\n\t\t\tCtime:    time.Unix(f.FileCreateTime, 0),\n\t\t\tModified: time.Unix(f.FileUpdateTime, 0),\n\t\t\tIsFolder: f.IsDir == 1,\n\t\t\tHashInfo: utils.NewHashInfo(hash_extend.GCID, f.Hash),\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: f.Thumb,\n\t\t},\n\t}\n}\n\ntype FileDownloadResp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\tData []struct {\n\t\tError       int    `json:\"error\"`\n\t\tDownloadURL string `json:\"download_url\"`\n\t\tHash        string `json:\"hash\"`\n\t\tHashType    string `json:\"hash_type\"`\n\t\tFid         int    `json:\"fid\"`\n\t\tFileName    string `json:\"file_name\"`\n\t\tParentID    int    `json:\"parent_id\"`\n\t\tFileSize    int    `json:\"file_size\"`\n\t\tExt         string `json:\"ext\"`\n\t\tThumb       string `json:\"thumb\"`\n\t\tVipLink     int    `json:\"vip_link\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/febbox/util.go",
    "content": "package febbox\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *FebBox) refreshTokenByOAuth2() error {\n\ttoken, err := d.oauth2Token.Token()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Status = \"work\"\n\td.accessToken = token.AccessToken\n\td.Addition.RefreshToken = token.RefreshToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *FebBox) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\t// 使用oauth2 获取 access_token\n\ttoken, err := d.oauth2Token.Token()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch e.ErrorCode {\n\tcase 0:\n\t\treturn res.Body(), nil\n\tcase 1:\n\t\treturn res.Body(), nil\n\tcase -10001:\n\t\tif e.ServerName != \"\" {\n\t\t\t// access_token 过期\n\t\t\tif err = d.refreshTokenByOAuth2(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(url, method, callback, resp)\n\t\t} else {\n\t\t\treturn nil, errors.New(e.Error())\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(e.Error())\n\t}\n}\n\nfunc (d *FebBox) getFilesList(id string) ([]File, error) {\n\tif d.PageSize <= 0 {\n\t\td.PageSize = 100\n\t}\n\tres, err := d.listWithLimit(id, d.PageSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn *res, nil\n}\n\nfunc (d *FebBox) listWithLimit(dirID string, pageLimit int64) (*[]File, error) {\n\tvar files []File\n\tpage := int64(1)\n\tfor {\n\t\tresult, err := d.getFiles(dirID, page, pageLimit)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfiles = append(files, *result...)\n\t\tif int64(len(*result)) < pageLimit {\n\t\t\tbreak\n\t\t} else {\n\t\t\tpage++\n\t\t}\n\t}\n\treturn &files, nil\n}\n\nfunc (d *FebBox) getFiles(dirID string, page, pageLimit int64) (*[]File, error) {\n\tvar fileList FileListResp\n\tqueryParams := map[string]string{\n\t\t\"module\":    \"file_list\",\n\t\t\"parent_id\": dirID,\n\t\t\"page\":      strconv.FormatInt(page, 10),\n\t\t\"pagelimit\": strconv.FormatInt(pageLimit, 10),\n\t\t\"order\":     d.Addition.SortRule,\n\t}\n\n\tres, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, &fileList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = json.Unmarshal(res, &fileList); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &fileList.Data.FileList, nil\n}\n\nfunc (d *FebBox) getDownloadLink(id string, ip string) (string, error) {\n\tvar fileDownloadResp FileDownloadResp\n\tqueryParams := map[string]string{\n\t\t\"module\": \"file_get_download_url\",\n\t\t\"fids[]\": id,\n\t\t\"ip\":     ip,\n\t}\n\n\tres, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, &fileDownloadResp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = json.Unmarshal(res, &fileDownloadResp); err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(fileDownloadResp.Data) == 0 {\n\t\treturn \"\", fmt.Errorf(\"can not get download link, code:%d, msg:%s\", fileDownloadResp.Code, fileDownloadResp.Msg)\n\t}\n\n\treturn fileDownloadResp.Data[0].DownloadURL, nil\n}\n\nfunc (d *FebBox) makeDir(id string, name string) error {\n\tqueryParams := map[string]string{\n\t\t\"module\":    \"create_dir\",\n\t\t\"parent_id\": id,\n\t\t\"name\":      name,\n\t}\n\n\t_, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *FebBox) move(id string, id2 string) error {\n\tqueryParams := map[string]string{\n\t\t\"module\": \"file_move\",\n\t\t\"fids[]\": id,\n\t\t\"to\":     id2,\n\t}\n\n\t_, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *FebBox) rename(id string, name string) error {\n\tqueryParams := map[string]string{\n\t\t\"module\": \"file_rename\",\n\t\t\"fid\":    id,\n\t\t\"name\":   name,\n\t}\n\n\t_, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *FebBox) copy(id string, id2 string) error {\n\tqueryParams := map[string]string{\n\t\t\"module\": \"file_copy\",\n\t\t\"fids[]\": id,\n\t\t\"to\":     id2,\n\t}\n\n\t_, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *FebBox) remove(id string) error {\n\tqueryParams := map[string]string{\n\t\t\"module\": \"file_delete\",\n\t\t\"fids[]\": id,\n\t}\n\n\t_, err := d.request(\"https://api.febbox.com/oauth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetMultipartFormData(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/ftp/driver.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/jlaffaye/ftp\"\n)\n\ntype FTP struct {\n\tmodel.Storage\n\tAddition\n\tconn *ftp.ServerConn\n\n\tctx    context.Context\n\tcancel context.CancelFunc\n}\n\nfunc (d *FTP) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *FTP) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *FTP) Init(ctx context.Context) error {\n\td.ctx, d.cancel = context.WithCancel(context.Background())\n\tvar err error\n\td.conn, err = d._login(ctx)\n\treturn err\n}\n\nfunc (d *FTP) Drop(ctx context.Context) error {\n\tif d.conn != nil {\n\t\t_ = d.conn.Quit()\n\t\td.cancel()\n\t}\n\treturn nil\n}\n\nfunc (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.login(); err != nil {\n\t\treturn nil, err\n\t}\n\tentries, err := d.conn.List(encode(dir.GetPath(), d.Encoding))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]model.Obj, 0)\n\tfor _, entry := range entries {\n\t\tif entry.Name == \".\" || entry.Name == \"..\" {\n\t\t\tcontinue\n\t\t}\n\t\tname := decode(entry.Name, d.Encoding)\n\t\tf := model.Object{\n\t\t\tName:     name,\n\t\t\tSize:     int64(entry.Size),\n\t\t\tModified: entry.Time,\n\t\t\tIsFolder: entry.Type == ftp.EntryTypeFolder,\n\t\t\tPath:     stdpath.Join(dir.GetPath(), name),\n\t\t}\n\t\tres = append(res, &f)\n\t}\n\treturn res, nil\n}\n\nfunc (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tconn, err := d._login(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpath := encode(file.GetPath(), d.Encoding)\n\tsize := file.GetSize()\n\tresultRangeReader := func(context context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tlength := httpRange.Length\n\t\tif length < 0 || httpRange.Start+length > size {\n\t\t\tlength = size - httpRange.Start\n\t\t}\n\t\tvar c *ftp.ServerConn\n\t\tif ctx == context {\n\t\t\tc = conn\n\t\t} else {\n\t\t\tvar err error\n\t\t\tc, err = d._login(context)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tresp, err := c.RetrFrom(path, uint64(httpRange.Start))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar close utils.CloseFunc\n\t\tif context == ctx {\n\t\t\tclose = resp.Close\n\t\t} else {\n\t\t\tclose = func() error {\n\t\t\t\treturn errors.Join(resp.Close(), c.Quit())\n\t\t\t}\n\t\t}\n\t\treturn utils.ReadCloser{\n\t\t\tReader: io.LimitReader(resp, length),\n\t\t\tCloser: close,\n\t\t}, nil\n\t}\n\n\treturn &model.Link{\n\t\tRangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),\n\t\tSyncClosers: utils.NewSyncClosers(utils.CloseFunc(conn.Quit)),\n\t}, nil\n}\n\nfunc (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\treturn d.conn.MakeDir(encode(stdpath.Join(parentDir.GetPath(), dirName), d.Encoding))\n}\n\nfunc (d *FTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\treturn d.conn.Rename(\n\t\tencode(srcObj.GetPath(), d.Encoding),\n\t\tencode(stdpath.Join(dstDir.GetPath(), srcObj.GetName()), d.Encoding),\n\t)\n}\n\nfunc (d *FTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\treturn d.conn.Rename(\n\t\tencode(srcObj.GetPath(), d.Encoding),\n\t\tencode(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName), d.Encoding),\n\t)\n}\n\nfunc (d *FTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *FTP) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\tpath := encode(obj.GetPath(), d.Encoding)\n\tif obj.IsDir() {\n\t\treturn d.conn.RemoveDirRecur(path)\n\t} else {\n\t\treturn d.conn.Delete(path)\n\t}\n}\n\nfunc (d *FTP) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\tpath := stdpath.Join(dstDir.GetPath(), s.GetName())\n\treturn d.conn.Stor(encode(path, d.Encoding), driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t}))\n}\n\nvar _ driver.Driver = (*FTP)(nil)\n"
  },
  {
    "path": "drivers/ftp/meta.go",
    "content": "package ftp\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/axgle/mahonia\"\n)\n\nfunc encode(str string, encoding string) string {\n\tif encoding == \"\" {\n\t\treturn str\n\t}\n\tencoder := mahonia.NewEncoder(encoding)\n\treturn encoder.ConvertString(str)\n}\n\nfunc decode(str string, encoding string) string {\n\tif encoding == \"\" {\n\t\treturn str\n\t}\n\tdecoder := mahonia.NewDecoder(encoding)\n\treturn decoder.ConvertString(str)\n}\n\ntype Addition struct {\n\tAddress  string `json:\"address\" required:\"true\"`\n\tEncoding string `json:\"encoding\" required:\"true\"`\n\tUsername string `json:\"username\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\tdriver.RootPath\n}\n\nvar config = driver.Config{\n\tName:        \"FTP\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &FTP{}\n\t})\n}\n"
  },
  {
    "path": "drivers/ftp/types.go",
    "content": "package ftp\n"
  },
  {
    "path": "drivers/ftp/util.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/jlaffaye/ftp\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *FTP) login() error {\n\t_, err, _ := singleflight.AnyGroup.Do(fmt.Sprintf(\"FTP.login:%p\", d), func() (any, error) {\n\t\tvar err error\n\t\tif d.conn != nil {\n\t\t\terr = d.conn.NoOp()\n\t\t\tif err != nil {\n\t\t\t\td.conn.Quit()\n\t\t\t\td.conn = nil\n\t\t\t}\n\t\t}\n\t\tif d.conn == nil {\n\t\t\td.conn, err = d._login(d.ctx)\n\t\t}\n\t\treturn nil, err\n\t})\n\treturn err\n}\n\nfunc (d *FTP) _login(ctx context.Context) (*ftp.ServerConn, error) {\n\tconn, err := ftp.Dial(d.Address, ftp.DialWithShutTimeout(10*time.Second), ftp.DialWithContext(ctx))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = conn.Login(d.Username, d.Password)\n\tif err != nil {\n\t\tconn.Quit()\n\t\treturn nil, err\n\t}\n\treturn conn, nil\n}\n"
  },
  {
    "path": "drivers/github/driver.go",
    "content": "package github\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Github struct {\n\tmodel.Storage\n\tAddition\n\tclient        *resty.Client\n\tmkdirMsgTmpl  *template.Template\n\tdeleteMsgTmpl *template.Template\n\tputMsgTmpl    *template.Template\n\trenameMsgTmpl *template.Template\n\tcopyMsgTmpl   *template.Template\n\tmoveMsgTmpl   *template.Template\n\tisOnBranch    bool\n\tcommitMutex   sync.Mutex\n\tpgpEntity     *openpgp.Entity\n}\n\nfunc (d *Github) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Github) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Github) Init(ctx context.Context) error {\n\td.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)\n\tif d.CommitterName != \"\" && d.CommitterEmail == \"\" {\n\t\treturn errors.New(\"committer email is required\")\n\t}\n\tif d.CommitterName == \"\" && d.CommitterEmail != \"\" {\n\t\treturn errors.New(\"committer name is required\")\n\t}\n\tif d.AuthorName != \"\" && d.AuthorEmail == \"\" {\n\t\treturn errors.New(\"author email is required\")\n\t}\n\tif d.AuthorName == \"\" && d.AuthorEmail != \"\" {\n\t\treturn errors.New(\"author name is required\")\n\t}\n\tvar err error\n\td.mkdirMsgTmpl, err = template.New(\"mkdirCommitMsgTemplate\").Parse(d.MkdirCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.deleteMsgTmpl, err = template.New(\"deleteCommitMsgTemplate\").Parse(d.DeleteCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.putMsgTmpl, err = template.New(\"putCommitMsgTemplate\").Parse(d.PutCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.renameMsgTmpl, err = template.New(\"renameCommitMsgTemplate\").Parse(d.RenameCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.copyMsgTmpl, err = template.New(\"copyCommitMsgTemplate\").Parse(d.CopyCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.moveMsgTmpl, err = template.New(\"moveCommitMsgTemplate\").Parse(d.MoveCommitMsg)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.client = base.NewRestyClient().\n\t\tSetHeader(\"Accept\", \"application/vnd.github.object+json\").\n\t\tSetHeader(\"X-GitHub-Api-Version\", \"2022-11-28\").\n\t\tSetLogger(log.StandardLogger()).\n\t\tSetDebug(false)\n\ttoken := strings.TrimSpace(d.Token)\n\tif token != \"\" {\n\t\td.client = d.client.SetHeader(\"Authorization\", \"Bearer \"+token)\n\t}\n\tif d.Ref == \"\" {\n\t\trepo, err := d.getRepo()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.Ref = repo.DefaultBranch\n\t\td.isOnBranch = true\n\t} else {\n\t\t_, err = d.getBranchHead()\n\t\td.isOnBranch = err == nil\n\t}\n\tif d.GPGPrivateKey != \"\" {\n\t\tif d.CommitterName == \"\" || d.AuthorName == \"\" {\n\t\t\tuser, e := d.getAuthenticatedUser()\n\t\t\tif e != nil {\n\t\t\t\treturn e\n\t\t\t}\n\t\t\tif d.CommitterName == \"\" {\n\t\t\t\td.CommitterName = user.Name\n\t\t\t\td.CommitterEmail = user.Email\n\t\t\t}\n\t\t\tif d.AuthorName == \"\" {\n\t\t\t\td.AuthorName = user.Name\n\t\t\t\td.AuthorEmail = user.Email\n\t\t\t}\n\t\t}\n\t\td.pgpEntity, err = loadPrivateKey(d.GPGPrivateKey, d.GPGKeyPassphrase)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Github) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Github) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tobj, err := d.get(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif obj.Entries == nil {\n\t\treturn nil, errs.NotFolder\n\t}\n\tif len(obj.Entries) >= 1000 {\n\t\ttree, err := d.getTree(obj.Sha)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif tree.Truncated {\n\t\t\treturn nil, fmt.Errorf(\"tree %s is truncated\", dir.GetPath())\n\t\t}\n\t\tret := make([]model.Obj, 0, len(tree.Trees))\n\t\tfor _, t := range tree.Trees {\n\t\t\tif t.Path != \".gitkeep\" {\n\t\t\t\tret = append(ret, t.toModelObj())\n\t\t\t}\n\t\t}\n\t\treturn ret, nil\n\t} else {\n\t\tret := make([]model.Obj, 0, len(obj.Entries))\n\t\tfor _, entry := range obj.Entries {\n\t\t\tif entry.Name != \".gitkeep\" {\n\t\t\t\tret = append(ret, entry.toModelObj())\n\t\t\t}\n\t\t}\n\t\treturn ret, nil\n\t}\n}\n\nfunc (d *Github) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tobj, err := d.get(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif obj.Type == \"submodule\" {\n\t\treturn nil, errors.New(\"cannot download a submodule\")\n\t}\n\turl := obj.DownloadURL\n\tghProxy := strings.TrimSpace(d.Addition.GitHubProxy)\n\tif ghProxy != \"\" {\n\t\turl = strings.Replace(url, \"https://raw.githubusercontent.com\", ghProxy, 1)\n\t}\n\treturn &model.Link{\n\t\tURL: url,\n\t}, nil\n}\n\nfunc (d *Github) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\tparent, err := d.get(parentDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif parent.Entries == nil {\n\t\treturn errs.NotFolder\n\t}\n\tsubDirSha, err := d.newTree(\"\", []interface{}{\n\t\tmap[string]string{\n\t\t\t\"path\":    \".gitkeep\",\n\t\t\t\"mode\":    \"100644\",\n\t\t\t\"type\":    \"blob\",\n\t\t\t\"content\": \"\",\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, TreeObjReq{\n\t\tPath: dirName,\n\t\tMode: \"040000\",\n\t\tType: \"tree\",\n\t\tSha:  subDirSha,\n\t})\n\tif len(parent.Entries) == 1 && parent.Entries[0].Name == \".gitkeep\" {\n\t\tnewTree = append(newTree, TreeObjReq{\n\t\t\tPath: \".gitkeep\",\n\t\t\tMode: \"100644\",\n\t\t\tType: \"blob\",\n\t\t\tSha:  nil,\n\t\t})\n\t}\n\tnewSha, err := d.newTree(parent.Sha, newTree)\n\tif err != nil {\n\t\treturn err\n\t}\n\trootSha, err := d.renewParentTrees(parentDir.GetPath(), parent.Sha, newSha, \"/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcommitMessage, err := getMessage(d.mkdirMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    dirName,\n\t\tObjPath:    stdpath.Join(parentDir.GetPath(), dirName),\n\t\tParentName: parentDir.GetName(),\n\t\tParentPath: parentDir.GetPath(),\n\t}, \"mkdir\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(commitMessage, rootSha)\n}\n\nfunc (d *Github) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\tif strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {\n\t\treturn errors.New(\"cannot move parent dir to child\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\n\tvar rootSha string\n\tif strings.HasPrefix(dstDir.GetPath(), stdpath.Dir(srcObj.GetPath())) { // /aa/1 -> /aa/bb/\n\t\tdstOldSha, dstNewSha, ancestorOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsrcParentPath := stdpath.Dir(srcObj.GetPath())\n\t\tdstRest := dstDir.GetPath()[len(srcParentPath):]\n\t\tif dstRest[0] == '/' {\n\t\t\tdstRest = dstRest[1:]\n\t\t}\n\t\tdstNextName, _, _ := strings.Cut(dstRest, \"/\")\n\t\tdstNextPath := stdpath.Join(srcParentPath, dstNextName)\n\t\tdstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, dstNextPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar delSrc, dstNextTree *TreeObjReq = nil, nil\n\t\tfor _, t := range srcParentTree.Trees {\n\t\t\tif t.Path == dstNextName {\n\t\t\t\tdstNextTree = &t.TreeObjReq\n\t\t\t\tdstNextTree.Sha = dstNextTreeSha\n\t\t\t}\n\t\t\tif t.Path == srcObj.GetName() {\n\t\t\t\tdelSrc = &t.TreeObjReq\n\t\t\t\tdelSrc.Sha = nil\n\t\t\t}\n\t\t\tif delSrc != nil && dstNextTree != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif delSrc == nil || dstNextTree == nil {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\t\tancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*delSrc, *dstNextTree})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trootSha, err = d.renewParentTrees(srcParentPath, ancestorOldSha, ancestorNewSha, \"/\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if strings.HasPrefix(srcObj.GetPath(), dstDir.GetPath()) { // /aa/bb/1 -> /aa/\n\t\tsrcParentPath := stdpath.Dir(srcObj.GetPath())\n\t\tsrcParentTree, srcParentOldSha, err := d.getTreeDirectly(srcParentPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar src *TreeObjReq = nil\n\t\tfor _, t := range srcParentTree.Trees {\n\t\t\tif t.Path == srcObj.GetName() {\n\t\t\t\tif t.Type == \"commit\" {\n\t\t\t\t\treturn errors.New(\"cannot move a submodule\")\n\t\t\t\t}\n\t\t\t\tsrc = &t.TreeObjReq\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif src == nil {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\n\t\tdelSrc := *src\n\t\tdelSrc.Sha = nil\n\t\tdelSrcTree := make([]interface{}, 0, 2)\n\t\tdelSrcTree = append(delSrcTree, delSrc)\n\t\tif len(srcParentTree.Trees) == 1 {\n\t\t\tdelSrcTree = append(delSrcTree, map[string]string{\n\t\t\t\t\"path\":    \".gitkeep\",\n\t\t\t\t\"mode\":    \"100644\",\n\t\t\t\t\"type\":    \"blob\",\n\t\t\t\t\"content\": \"\",\n\t\t\t})\n\t\t}\n\t\tsrcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsrcRest := srcObj.GetPath()[len(dstDir.GetPath()):]\n\t\tif srcRest[0] == '/' {\n\t\t\tsrcRest = srcRest[1:]\n\t\t}\n\t\tsrcNextName, _, ok := strings.Cut(srcRest, \"/\")\n\t\tif !ok { // /aa/1 -> /aa/\n\t\t\treturn errors.New(\"cannot move in place\")\n\t\t}\n\t\tsrcNextPath := stdpath.Join(dstDir.GetPath(), srcNextName)\n\t\tsrcNextTreeSha, err := d.renewParentTrees(srcParentPath, srcParentOldSha, srcParentNewSha, srcNextPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tancestorTree, ancestorOldSha, err := d.getTreeDirectly(dstDir.GetPath())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar srcNextTree *TreeObjReq = nil\n\t\tfor _, t := range ancestorTree.Trees {\n\t\t\tif t.Path == srcNextName {\n\t\t\t\tsrcNextTree = &t.TreeObjReq\n\t\t\t\tsrcNextTree.Sha = srcNextTreeSha\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif srcNextTree == nil {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\t\tancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*srcNextTree, *src})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trootSha, err = d.renewParentTrees(dstDir.GetPath(), ancestorOldSha, ancestorNewSha, \"/\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else { // /aa/1 -> /bb/\n\t\t// do copy\n\t\tdstOldSha, dstNewSha, srcParentOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// delete src object and create new tree\n\t\tvar srcNewTree *TreeObjReq = nil\n\t\tfor _, t := range srcParentTree.Trees {\n\t\t\tif t.Path == srcObj.GetName() {\n\t\t\t\tsrcNewTree = &t.TreeObjReq\n\t\t\t\tsrcNewTree.Sha = nil\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif srcNewTree == nil {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\t\tdelSrcTree := make([]interface{}, 0, 2)\n\t\tdelSrcTree = append(delSrcTree, *srcNewTree)\n\t\tif len(srcParentTree.Trees) == 1 {\n\t\t\tdelSrcTree = append(delSrcTree, map[string]string{\n\t\t\t\t\"path\":    \".gitkeep\",\n\t\t\t\t\"mode\":    \"100644\",\n\t\t\t\t\"type\":    \"blob\",\n\t\t\t\t\"content\": \"\",\n\t\t\t})\n\t\t}\n\t\tsrcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// renew but the common ancestor of srcPath and dstPath\n\t\tancestor, srcChildName, dstChildName, _, _ := getPathCommonAncestor(srcObj.GetPath(), dstDir.GetPath())\n\t\tdstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, stdpath.Join(ancestor, dstChildName))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsrcNextTreeSha, err := d.renewParentTrees(stdpath.Dir(srcObj.GetPath()), srcParentOldSha, srcParentNewSha, stdpath.Join(ancestor, srcChildName))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// renew the tree of the last common ancestor\n\t\tancestorTree, ancestorOldSha, err := d.getTreeDirectly(ancestor)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewTree := make([]interface{}, 2)\n\t\tsrcBind := false\n\t\tdstBind := false\n\t\tfor _, t := range ancestorTree.Trees {\n\t\t\tif t.Path == srcChildName {\n\t\t\t\tt.Sha = srcNextTreeSha\n\t\t\t\tnewTree[0] = t.TreeObjReq\n\t\t\t\tsrcBind = true\n\t\t\t}\n\t\t\tif t.Path == dstChildName {\n\t\t\t\tt.Sha = dstNextTreeSha\n\t\t\t\tnewTree[1] = t.TreeObjReq\n\t\t\t\tdstBind = true\n\t\t\t}\n\t\t\tif srcBind && dstBind {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !srcBind || !dstBind {\n\t\t\treturn errs.ObjectNotFound\n\t\t}\n\t\tancestorNewSha, err := d.newTree(ancestorOldSha, newTree)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// renew until root\n\t\trootSha, err = d.renewParentTrees(ancestor, ancestorOldSha, ancestorNewSha, \"/\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// commit\n\tmessage, err := getMessage(d.moveMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    srcObj.GetName(),\n\t\tObjPath:    srcObj.GetPath(),\n\t\tParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())),\n\t\tParentPath: stdpath.Dir(srcObj.GetPath()),\n\t\tTargetName: stdpath.Base(dstDir.GetPath()),\n\t\tTargetPath: dstDir.GetPath(),\n\t}, \"move\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(message, rootSha)\n}\n\nfunc (d *Github) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\tparentDir := stdpath.Dir(srcObj.GetPath())\n\ttree, _, err := d.getTreeDirectly(parentDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnewTree := make([]interface{}, 2)\n\toperated := false\n\tfor _, t := range tree.Trees {\n\t\tif t.Path == srcObj.GetName() {\n\t\t\tif t.Type == \"commit\" {\n\t\t\t\treturn errors.New(\"cannot rename a submodule\")\n\t\t\t}\n\t\t\tdelCopy := t.TreeObjReq\n\t\t\tdelCopy.Sha = nil\n\t\t\tnewTree[0] = delCopy\n\t\t\tt.Path = newName\n\t\t\tnewTree[1] = t.TreeObjReq\n\t\t\toperated = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !operated {\n\t\treturn errs.ObjectNotFound\n\t}\n\tnewSha, err := d.newTree(tree.Sha, newTree)\n\tif err != nil {\n\t\treturn err\n\t}\n\trootSha, err := d.renewParentTrees(parentDir, tree.Sha, newSha, \"/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tmessage, err := getMessage(d.renameMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    srcObj.GetName(),\n\t\tObjPath:    srcObj.GetPath(),\n\t\tParentName: stdpath.Base(parentDir),\n\t\tParentPath: parentDir,\n\t\tTargetName: newName,\n\t\tTargetPath: stdpath.Join(parentDir, newName),\n\t}, \"rename\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(message, rootSha)\n}\n\nfunc (d *Github) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\tif strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {\n\t\treturn errors.New(\"cannot copy parent dir to child\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\n\tdstSha, newSha, _, _, err := d.copyWithoutRenewTree(srcObj, dstDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\trootSha, err := d.renewParentTrees(dstDir.GetPath(), dstSha, newSha, \"/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tmessage, err := getMessage(d.copyMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    srcObj.GetName(),\n\t\tObjPath:    srcObj.GetPath(),\n\t\tParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())),\n\t\tParentPath: stdpath.Dir(srcObj.GetPath()),\n\t\tTargetName: stdpath.Base(dstDir.GetPath()),\n\t\tTargetPath: dstDir.GetPath(),\n\t}, \"copy\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(message, rootSha)\n}\n\nfunc (d *Github) Remove(ctx context.Context, obj model.Obj) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\tparentDir := stdpath.Dir(obj.GetPath())\n\ttree, treeSha, err := d.getTreeDirectly(parentDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar del *TreeObjReq = nil\n\tfor _, t := range tree.Trees {\n\t\tif t.Path == obj.GetName() {\n\t\t\tif t.Type == \"commit\" {\n\t\t\t\treturn errors.New(\"cannot remove a submodule\")\n\t\t\t}\n\t\t\tdel = &t.TreeObjReq\n\t\t\tdel.Sha = nil\n\t\t\tbreak\n\t\t}\n\t}\n\tif del == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, *del)\n\tif len(tree.Trees) == 1 { // completely emptying the repository will get a 404\n\t\tnewTree = append(newTree, map[string]string{\n\t\t\t\"path\":    \".gitkeep\",\n\t\t\t\"mode\":    \"100644\",\n\t\t\t\"type\":    \"blob\",\n\t\t\t\"content\": \"\",\n\t\t})\n\t}\n\tnewSha, err := d.newTree(treeSha, newTree)\n\tif err != nil {\n\t\treturn err\n\t}\n\trootSha, err := d.renewParentTrees(parentDir, treeSha, newSha, \"/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tcommitMessage, err := getMessage(d.deleteMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    obj.GetName(),\n\t\tObjPath:    obj.GetPath(),\n\t\tParentName: stdpath.Base(parentDir),\n\t\tParentPath: parentDir,\n\t}, \"remove\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(commitMessage, rootSha)\n}\n\nfunc (d *Github) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\tblob, err := d.putBlob(ctx, stream, up)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\tparent, err := d.get(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif parent.Entries == nil {\n\t\treturn errs.NotFolder\n\t}\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, TreeObjReq{\n\t\tPath: stream.GetName(),\n\t\tMode: \"100644\",\n\t\tType: \"blob\",\n\t\tSha:  blob,\n\t})\n\tif len(parent.Entries) == 1 && parent.Entries[0].Name == \".gitkeep\" {\n\t\tnewTree = append(newTree, TreeObjReq{\n\t\t\tPath: \".gitkeep\",\n\t\t\tMode: \"100644\",\n\t\t\tType: \"blob\",\n\t\t\tSha:  nil,\n\t\t})\n\t}\n\tnewSha, err := d.newTree(parent.Sha, newTree)\n\tif err != nil {\n\t\treturn err\n\t}\n\trootSha, err := d.renewParentTrees(dstDir.GetPath(), parent.Sha, newSha, \"/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcommitMessage, err := getMessage(d.putMsgTmpl, &MessageTemplateVars{\n\t\tUserName:   getUsername(ctx),\n\t\tObjName:    stream.GetName(),\n\t\tObjPath:    stdpath.Join(dstDir.GetPath(), stream.GetName()),\n\t\tParentName: dstDir.GetName(),\n\t\tParentPath: dstDir.GetPath(),\n\t}, \"upload\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.commit(commitMessage, rootSha)\n}\n\nvar _ driver.Driver = (*Github)(nil)\n\nfunc (d *Github) getContentApiUrl(path string) string {\n\tpath = utils.FixAndCleanPath(path)\n\treturn fmt.Sprintf(\"https://api.github.com/repos/%s/%s/contents%s\", d.Owner, d.Repo, path)\n}\n\nfunc (d *Github) get(path string) (*Object, error) {\n\tres, err := d.client.R().SetQueryParam(\"ref\", d.Ref).Get(d.getContentApiUrl(path))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn nil, toErr(res)\n\t}\n\tvar resp Object\n\terr = utils.Json.Unmarshal(res.Body(), &resp)\n\treturn &resp, err\n}\n\nfunc (d *Github) putBlob(ctx context.Context, s model.FileStreamer, up driver.UpdateProgress) (string, error) {\n\tbeforeContent := \"{\\\"encoding\\\":\\\"base64\\\",\\\"content\\\":\\\"\"\n\tafterContent := \"\\\"}\"\n\tlength := int64(len(beforeContent)) + calculateBase64Length(s.GetSize()) + int64(len(afterContent))\n\tbeforeContentReader := strings.NewReader(beforeContent)\n\tcontentReader, contentWriter := io.Pipe()\n\tgo func() {\n\t\tencoder := base64.NewEncoder(base64.StdEncoding, contentWriter)\n\t\tif _, err := utils.CopyWithBuffer(encoder, s); err != nil {\n\t\t\t_ = contentWriter.CloseWithError(err)\n\t\t\treturn\n\t\t}\n\t\t_ = encoder.Close()\n\t\t_ = contentWriter.Close()\n\t}()\n\tafterContentReader := strings.NewReader(afterContent)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost,\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%s/%s/git/blobs\", d.Owner, d.Repo),\n\t\tdriver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\t\tReader: io.MultiReader(beforeContentReader, contentReader, afterContentReader),\n\t\t\t\tSize:   length,\n\t\t\t},\n\t\t\tUpdateProgress: up,\n\t\t}))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\treq.Header.Set(\"X-GitHub-Api-Version\", \"2022-11-28\")\n\ttoken := strings.TrimSpace(d.Token)\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t}\n\treq.ContentLength = length\n\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\tresBody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif res.StatusCode != 201 {\n\t\tvar errMsg ErrResp\n\t\tif err = utils.Json.Unmarshal(resBody, &errMsg); err != nil {\n\t\t\treturn \"\", errors.New(res.Status)\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"%s: %s\", res.Status, errMsg.Message)\n\t\t}\n\t}\n\tvar resp PutBlobResp\n\tif err = utils.Json.Unmarshal(resBody, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Sha, nil\n}\n\nfunc (d *Github) renewParentTrees(path, prevSha, curSha, until string) (string, error) {\n\tfor path != until {\n\t\tpath = stdpath.Dir(path)\n\t\ttree, sha, err := d.getTreeDirectly(path)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tvar newTree *TreeObjReq = nil\n\t\tfor _, t := range tree.Trees {\n\t\t\tif t.Sha == prevSha {\n\t\t\t\tnewTree = &t.TreeObjReq\n\t\t\t\tnewTree.Sha = curSha\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif newTree == nil {\n\t\t\treturn \"\", errs.ObjectNotFound\n\t\t}\n\t\tcurSha, err = d.newTree(sha, []interface{}{*newTree})\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tprevSha = sha\n\t}\n\treturn curSha, nil\n}\n\nfunc (d *Github) getTree(sha string) (*TreeResp, error) {\n\tres, err := d.client.R().Get(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/git/trees/%s\", d.Owner, d.Repo, sha))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn nil, toErr(res)\n\t}\n\tvar resp TreeResp\n\tif err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Github) getTreeDirectly(path string) (*TreeResp, string, error) {\n\tp, err := d.get(path)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif p.Entries == nil {\n\t\treturn nil, \"\", fmt.Errorf(\"%s is not a folder\", path)\n\t}\n\ttree, err := d.getTree(p.Sha)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif tree.Truncated {\n\t\treturn nil, \"\", fmt.Errorf(\"tree %s is truncated\", path)\n\t}\n\treturn tree, p.Sha, nil\n}\n\nfunc (d *Github) newTree(baseSha string, tree []interface{}) (string, error) {\n\tbody := &TreeReq{Trees: tree}\n\tif baseSha != \"\" {\n\t\tbody.BaseTree = baseSha\n\t}\n\tres, err := d.client.R().SetBody(body).\n\t\tPost(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/git/trees\", d.Owner, d.Repo))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif res.StatusCode() != 201 {\n\t\treturn \"\", toErr(res)\n\t}\n\tvar resp TreeResp\n\tif err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Sha, nil\n}\n\nfunc (d *Github) commit(message, treeSha string) error {\n\toldCommit, err := d.getBranchHead()\n\tbody := map[string]interface{}{\n\t\t\"message\": message,\n\t\t\"tree\":    treeSha,\n\t\t\"parents\": []string{oldCommit},\n\t}\n\td.addCommitterAndAuthor(&body)\n\tif d.pgpEntity != nil {\n\t\tsignature, e := signCommit(&body, d.pgpEntity)\n\t\tif e != nil {\n\t\t\treturn e\n\t\t}\n\t\tbody[\"signature\"] = signature\n\t}\n\tres, err := d.client.R().SetBody(body).Post(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/git/commits\", d.Owner, d.Repo))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() != 201 {\n\t\treturn toErr(res)\n\t}\n\tvar resp CommitResp\n\tif err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn err\n\t}\n\n\t// update branch head\n\tres, err = d.client.R().\n\t\tSetBody(&UpdateRefReq{\n\t\t\tSha:   resp.Sha,\n\t\t\tForce: false,\n\t\t}).\n\t\tPatch(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/git/refs/heads/%s\", d.Owner, d.Repo, d.Ref))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn toErr(res)\n\t}\n\treturn nil\n}\n\nfunc (d *Github) getBranchHead() (string, error) {\n\tres, err := d.client.R().Get(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/branches/%s\", d.Owner, d.Repo, d.Ref))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn \"\", toErr(res)\n\t}\n\tvar resp BranchResp\n\tif err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Commit.Sha, nil\n}\n\nfunc (d *Github) copyWithoutRenewTree(srcObj, dstDir model.Obj) (dstSha, newSha, srcParentSha string, srcParentTree *TreeResp, err error) {\n\tdst, err := d.get(dstDir.GetPath())\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", nil, err\n\t}\n\tif dst.Entries == nil {\n\t\treturn \"\", \"\", \"\", nil, errs.NotFolder\n\t}\n\tdstSha = dst.Sha\n\tsrcParentPath := stdpath.Dir(srcObj.GetPath())\n\tsrcParentTree, srcParentSha, err = d.getTreeDirectly(srcParentPath)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", nil, err\n\t}\n\tvar src *TreeObjReq = nil\n\tfor _, t := range srcParentTree.Trees {\n\t\tif t.Path == srcObj.GetName() {\n\t\t\tif t.Type == \"commit\" {\n\t\t\t\treturn \"\", \"\", \"\", nil, errors.New(\"cannot copy a submodule\")\n\t\t\t}\n\t\t\tsrc = &t.TreeObjReq\n\t\t\tbreak\n\t\t}\n\t}\n\tif src == nil {\n\t\treturn \"\", \"\", \"\", nil, errs.ObjectNotFound\n\t}\n\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, *src)\n\tif len(dst.Entries) == 1 && dst.Entries[0].Name == \".gitkeep\" {\n\t\tnewTree = append(newTree, TreeObjReq{\n\t\t\tPath: \".gitkeep\",\n\t\t\tMode: \"100644\",\n\t\t\tType: \"blob\",\n\t\t\tSha:  nil,\n\t\t})\n\t}\n\tnewSha, err = d.newTree(dstSha, newTree)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", nil, err\n\t}\n\treturn dstSha, newSha, srcParentSha, srcParentTree, nil\n}\n\nfunc (d *Github) getRepo() (*RepoResp, error) {\n\tres, err := d.client.R().Get(fmt.Sprintf(\"https://api.github.com/repos/%s/%s\", d.Owner, d.Repo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn nil, toErr(res)\n\t}\n\tvar resp RepoResp\n\tif err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Github) getAuthenticatedUser() (*UserResp, error) {\n\tres, err := d.client.R().Get(\"https://api.github.com/user\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn nil, toErr(res)\n\t}\n\tresp := &UserResp{}\n\tif err = utils.Json.Unmarshal(res.Body(), resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (d *Github) addCommitterAndAuthor(m *map[string]interface{}) {\n\tif d.CommitterName != \"\" {\n\t\tcommitter := map[string]string{\n\t\t\t\"name\":  d.CommitterName,\n\t\t\t\"email\": d.CommitterEmail,\n\t\t}\n\t\t(*m)[\"committer\"] = committer\n\t}\n\tif d.AuthorName != \"\" {\n\t\tauthor := map[string]string{\n\t\t\t\"name\":  d.AuthorName,\n\t\t\t\"email\": d.AuthorEmail,\n\t\t}\n\t\t(*m)[\"author\"] = author\n\t}\n}\n"
  },
  {
    "path": "drivers/github/meta.go",
    "content": "package github\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tToken            string `json:\"token\" type:\"string\" required:\"true\"`\n\tOwner            string `json:\"owner\" type:\"string\" required:\"true\"`\n\tRepo             string `json:\"repo\" type:\"string\" required:\"true\"`\n\tRef              string `json:\"ref\" type:\"string\" help:\"A branch, a tag or a commit SHA, main branch by default.\"`\n\tGitHubProxy      string `json:\"gh_proxy\" type:\"string\" help:\"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com or https://gh-proxy.com/raw.githubusercontent.com\"`\n\tGPGPrivateKey    string `json:\"gpg_private_key\" type:\"text\"`\n\tGPGKeyPassphrase string `json:\"gpg_key_passphrase\" type:\"string\"`\n\tCommitterName    string `json:\"committer_name\" type:\"string\"`\n\tCommitterEmail   string `json:\"committer_email\" type:\"string\"`\n\tAuthorName       string `json:\"author_name\" type:\"string\"`\n\tAuthorEmail      string `json:\"author_email\" type:\"string\"`\n\tMkdirCommitMsg   string `json:\"mkdir_commit_message\" type:\"text\" default:\"{{.UserName}} mkdir {{.ObjPath}}\"`\n\tDeleteCommitMsg  string `json:\"delete_commit_message\" type:\"text\" default:\"{{.UserName}} remove {{.ObjPath}}\"`\n\tPutCommitMsg     string `json:\"put_commit_message\" type:\"text\" default:\"{{.UserName}} upload {{.ObjPath}}\"`\n\tRenameCommitMsg  string `json:\"rename_commit_message\" type:\"text\" default:\"{{.UserName}} rename {{.ObjPath}} to {{.TargetName}}\"`\n\tCopyCommitMsg    string `json:\"copy_commit_message\" type:\"text\" default:\"{{.UserName}} copy {{.ObjPath}} to {{.TargetPath}}\"`\n\tMoveCommitMsg    string `json:\"move_commit_message\" type:\"text\" default:\"{{.UserName}} move {{.ObjPath}} to {{.TargetPath}}\"`\n}\n\nvar config = driver.Config{\n\tName:        \"GitHub API\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Github{}\n\t})\n}\n"
  },
  {
    "path": "drivers/github/types.go",
    "content": "package github\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Links struct {\n\tGit  string `json:\"git\"`\n\tHtml string `json:\"html\"`\n\tSelf string `json:\"self\"`\n}\n\ntype Object struct {\n\tType            string   `json:\"type\"`\n\tEncoding        string   `json:\"encoding\" required:\"false\"`\n\tSize            int64    `json:\"size\"`\n\tName            string   `json:\"name\"`\n\tPath            string   `json:\"path\"`\n\tContent         string   `json:\"Content\" required:\"false\"`\n\tSha             string   `json:\"sha\"`\n\tURL             string   `json:\"url\"`\n\tGitURL          string   `json:\"git_url\"`\n\tHtmlURL         string   `json:\"html_url\"`\n\tDownloadURL     string   `json:\"download_url\"`\n\tEntries         []Object `json:\"entries\" required:\"false\"`\n\tLinks           Links    `json:\"_links\"`\n\tSubmoduleGitURL string   `json:\"submodule_git_url\" required:\"false\"`\n\tTarget          string   `json:\"target\" required:\"false\"`\n}\n\nfunc (o *Object) toModelObj() *model.Object {\n\treturn &model.Object{\n\t\tName:     o.Name,\n\t\tSize:     o.Size,\n\t\tModified: time.Unix(0, 0),\n\t\tIsFolder: o.Type == \"dir\",\n\t\tPath:     utils.FixAndCleanPath(o.Path),\n\t}\n}\n\ntype PutBlobResp struct {\n\tURL string `json:\"url\"`\n\tSha string `json:\"sha\"`\n}\n\ntype ErrResp struct {\n\tMessage          string `json:\"message\"`\n\tDocumentationURL string `json:\"documentation_url\"`\n\tStatus           string `json:\"status\"`\n}\n\ntype TreeObjReq struct {\n\tPath string      `json:\"path\"`\n\tMode string      `json:\"mode\"`\n\tType string      `json:\"type\"`\n\tSha  interface{} `json:\"sha\"`\n}\n\ntype TreeObjResp struct {\n\tTreeObjReq\n\tSize int64  `json:\"size\" required:\"false\"`\n\tURL  string `json:\"url\"`\n}\n\nfunc (o *TreeObjResp) toModelObj() *model.Object {\n\treturn &model.Object{\n\t\tName:     o.Path,\n\t\tSize:     o.Size,\n\t\tModified: time.Unix(0, 0),\n\t\tIsFolder: o.Type == \"tree\",\n\t\tPath:     utils.FixAndCleanPath(o.Path),\n\t}\n}\n\ntype TreeResp struct {\n\tSha       string        `json:\"sha\"`\n\tURL       string        `json:\"url\"`\n\tTrees     []TreeObjResp `json:\"tree\"`\n\tTruncated bool          `json:\"truncated\"`\n}\n\ntype TreeReq struct {\n\tBaseTree interface{}   `json:\"base_tree,omitempty\"`\n\tTrees    []interface{} `json:\"tree\"`\n}\n\ntype CommitResp struct {\n\tSha string `json:\"sha\"`\n}\n\ntype BranchResp struct {\n\tName   string     `json:\"name\"`\n\tCommit CommitResp `json:\"commit\"`\n}\n\ntype UpdateRefReq struct {\n\tSha   string `json:\"sha\"`\n\tForce bool   `json:\"force\"`\n}\n\ntype RepoResp struct {\n\tDefaultBranch string `json:\"default_branch\"`\n}\n\ntype UserResp struct {\n\tName  string `json:\"name\"`\n\tEmail string `json:\"email\"`\n}\n"
  },
  {
    "path": "drivers/github/util.go",
    "content": "package github\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/armor\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype MessageTemplateVars struct {\n\tUserName   string\n\tObjName    string\n\tObjPath    string\n\tParentName string\n\tParentPath string\n\tTargetName string\n\tTargetPath string\n}\n\nfunc getMessage(tmpl *template.Template, vars *MessageTemplateVars, defaultOpStr string) (string, error) {\n\tsb := strings.Builder{}\n\tif err := tmpl.Execute(&sb, vars); err != nil {\n\t\treturn fmt.Sprintf(\"%s %s %s\", vars.UserName, defaultOpStr, vars.ObjPath), err\n\t}\n\treturn sb.String(), nil\n}\n\nfunc calculateBase64Length(inputLength int64) int64 {\n\treturn 4 * ((inputLength + 2) / 3)\n}\n\nfunc toErr(res *resty.Response) error {\n\tvar errMsg ErrResp\n\tif err := utils.Json.Unmarshal(res.Body(), &errMsg); err != nil {\n\t\treturn errors.New(res.Status())\n\t} else {\n\t\treturn fmt.Errorf(\"%s: %s\", res.Status(), errMsg.Message)\n\t}\n}\n\n// Example input:\n// a = /aaa/bbb/ccc\n// b = /aaa/b11/ddd/ccc\n//\n// Output:\n// ancestor = /aaa\n// aChildName = bbb\n// bChildName = b11\n// aRest = bbb/ccc\n// bRest = b11/ddd/ccc\nfunc getPathCommonAncestor(a, b string) (ancestor, aChildName, bChildName, aRest, bRest string) {\n\ta = utils.FixAndCleanPath(a)\n\tb = utils.FixAndCleanPath(b)\n\tidx := 1\n\tfor idx < len(a) && idx < len(b) {\n\t\tif a[idx] != b[idx] {\n\t\t\tbreak\n\t\t}\n\t\tidx++\n\t}\n\taNextIdx := idx\n\tfor aNextIdx < len(a) {\n\t\tif a[aNextIdx] == '/' {\n\t\t\tbreak\n\t\t}\n\t\taNextIdx++\n\t}\n\tbNextIdx := idx\n\tfor bNextIdx < len(b) {\n\t\tif b[bNextIdx] == '/' {\n\t\t\tbreak\n\t\t}\n\t\tbNextIdx++\n\t}\n\tfor idx > 0 {\n\t\tif a[idx] == '/' {\n\t\t\tbreak\n\t\t}\n\t\tidx--\n\t}\n\tancestor = utils.FixAndCleanPath(a[:idx])\n\taChildName = a[idx+1 : aNextIdx]\n\tbChildName = b[idx+1 : bNextIdx]\n\taRest = a[idx+1:]\n\tbRest = b[idx+1:]\n\treturn ancestor, aChildName, bChildName, aRest, bRest\n}\n\nfunc getUsername(ctx context.Context) string {\n\tuser, ok := ctx.Value(conf.UserKey).(*model.User)\n\tif !ok {\n\t\treturn \"<system>\"\n\t}\n\treturn user.Username\n}\n\nfunc loadPrivateKey(key, passphrase string) (*openpgp.Entity, error) {\n\tentityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(entityList) < 1 {\n\t\treturn nil, fmt.Errorf(\"no keys found in key ring\")\n\t}\n\tentity := entityList[0]\n\n\tpass := []byte(passphrase)\n\tif entity.PrivateKey != nil && entity.PrivateKey.Encrypted {\n\t\tif err = entity.PrivateKey.Decrypt(pass); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"password incorrect: %+v\", err)\n\t\t}\n\t}\n\tfor _, subKey := range entity.Subkeys {\n\t\tif subKey.PrivateKey != nil && subKey.PrivateKey.Encrypted {\n\t\t\tif err = subKey.PrivateKey.Decrypt(pass); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"password incorrect: %+v\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn entity, nil\n}\n\nfunc signCommit(m *map[string]interface{}, entity *openpgp.Entity) (string, error) {\n\tvar commit strings.Builder\n\tcommit.WriteString(fmt.Sprintf(\"tree %s\\n\", (*m)[\"tree\"].(string)))\n\tparents := (*m)[\"parents\"].([]string)\n\tfor _, p := range parents {\n\t\tcommit.WriteString(fmt.Sprintf(\"parent %s\\n\", p))\n\t}\n\tnow := time.Now()\n\t_, offset := now.Zone()\n\thour := offset / 3600\n\tauthor := (*m)[\"author\"].(map[string]string)\n\tcommit.WriteString(fmt.Sprintf(\"author %s <%s> %d %+03d00\\n\", author[\"name\"], author[\"email\"], now.Unix(), hour))\n\tauthor[\"date\"] = now.Format(time.RFC3339)\n\tcommitter := (*m)[\"committer\"].(map[string]string)\n\tcommit.WriteString(fmt.Sprintf(\"committer %s <%s> %d %+03d00\\n\", committer[\"name\"], committer[\"email\"], now.Unix(), hour))\n\tcommitter[\"date\"] = now.Format(time.RFC3339)\n\tcommit.WriteString(fmt.Sprintf(\"\\n%s\", (*m)[\"message\"].(string)))\n\tdata := commit.String()\n\n\tvar sigBuffer bytes.Buffer\n\terr := openpgp.DetachSign(&sigBuffer, entity, strings.NewReader(data), nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"signing failed: %v\", err)\n\t}\n\tvar armoredSig bytes.Buffer\n\tarmorWriter, err := armor.Encode(&armoredSig, \"PGP SIGNATURE\", nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err = utils.CopyWithBuffer(armorWriter, &sigBuffer); err != nil {\n\t\treturn \"\", err\n\t}\n\t_ = armorWriter.Close()\n\treturn armoredSig.String(), nil\n}\n"
  },
  {
    "path": "drivers/github_releases/driver.go",
    "content": "package github_releases\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype GithubReleases struct {\n\tmodel.Storage\n\tAddition\n\n\tpoints []MountPoint\n}\n\nfunc (d *GithubReleases) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *GithubReleases) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *GithubReleases) Init(ctx context.Context) error {\n\td.ParseRepos(d.Addition.RepoStructure)\n\treturn nil\n}\n\nfunc (d *GithubReleases) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles := make([]File, 0)\n\tpath := fmt.Sprintf(\"/%s\", strings.Trim(dir.GetPath(), \"/\"))\n\n\tfor i := range d.points {\n\t\tpoint := &d.points[i]\n\n\t\tif !d.Addition.ShowAllVersion { // latest\n\t\t\tpoint.RequestRelease(d.GetRequest, args.Refresh)\n\n\t\t\tif point.Point == path { // 与仓库路径相同\n\t\t\t\tfiles = append(files, point.GetLatestRelease()...)\n\t\t\t\tif d.Addition.ShowReadme {\n\t\t\t\t\tfiles = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)\n\t\t\t\t}\n\t\t\t\tif d.Addition.ShowSourceCode {\n\t\t\t\t\tfiles = append(files, point.GetSourceCode()...)\n\t\t\t\t}\n\t\t\t} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录\n\t\t\t\tnextDir := GetNextDir(point.Point, path)\n\t\t\t\tif nextDir == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\thasSameDir := false\n\t\t\t\tfor index := range files {\n\t\t\t\t\tif files[index].GetName() == nextDir {\n\t\t\t\t\t\thasSameDir = true\n\t\t\t\t\t\tfiles[index].Size += point.GetLatestSize()\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !hasSameDir {\n\t\t\t\t\tfiles = append(files, File{\n\t\t\t\t\t\tPath:     stdpath.Join(path, nextDir),\n\t\t\t\t\t\tFileName: nextDir,\n\t\t\t\t\t\tSize:     point.GetLatestSize(),\n\t\t\t\t\t\tUpdateAt: point.Release.PublishedAt,\n\t\t\t\t\t\tCreateAt: point.Release.CreatedAt,\n\t\t\t\t\t\tType:     \"dir\",\n\t\t\t\t\t\tUrl:      \"\",\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t} else { // all version\n\t\t\tpoint.RequestReleases(d.GetRequest, args.Refresh)\n\n\t\t\tif point.Point == path { // 与仓库路径相同\n\t\t\t\tfiles = append(files, point.GetAllVersion()...)\n\t\t\t\tif d.Addition.ShowReadme {\n\t\t\t\t\tfiles = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)\n\t\t\t\t}\n\t\t\t} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录\n\t\t\t\tnextDir := GetNextDir(point.Point, path)\n\t\t\t\tif nextDir == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\thasSameDir := false\n\t\t\t\tfor index := range files {\n\t\t\t\t\tif files[index].GetName() == nextDir {\n\t\t\t\t\t\thasSameDir = true\n\t\t\t\t\t\tfiles[index].Size += point.GetAllVersionSize()\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !hasSameDir {\n\t\t\t\t\tfiles = append(files, File{\n\t\t\t\t\t\tFileName: nextDir,\n\t\t\t\t\t\tPath:     stdpath.Join(path, nextDir),\n\t\t\t\t\t\tSize:     point.GetAllVersionSize(),\n\t\t\t\t\t\tUpdateAt: (*point.Releases)[0].PublishedAt,\n\t\t\t\t\t\tCreateAt: (*point.Releases)[0].CreatedAt,\n\t\t\t\t\t\tType:     \"dir\",\n\t\t\t\t\t\tUrl:      \"\",\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} else if strings.HasPrefix(path, point.Point) { // 仓库目录的子目录\n\t\t\t\ttagName := GetNextDir(path, point.Point)\n\t\t\t\tif tagName == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfiles = append(files, point.GetReleaseByTagName(tagName)...)\n\n\t\t\t\tif d.Addition.ShowSourceCode {\n\t\t\t\t\tfiles = append(files, point.GetSourceCodeByTagName(tagName)...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn src, nil\n\t})\n}\n\nfunc (d *GithubReleases) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\turl := file.GetID()\n\tgh_proxy := strings.TrimSpace(d.Addition.GitHubProxy)\n\n\tif gh_proxy != \"\" {\n\t\turl = strings.Replace(url, \"https://github.com\", gh_proxy, 1)\n\t}\n\n\tlink := model.Link{\n\t\tURL:    url,\n\t\tHeader: http.Header{},\n\t}\n\treturn &link, nil\n}\n\nfunc (d *GithubReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\t// TODO create folder, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *GithubReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO move obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *GithubReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\t// TODO rename obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *GithubReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO copy obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *GithubReleases) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotImplement\n}\n"
  },
  {
    "path": "drivers/github_releases/meta.go",
    "content": "package github_releases\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tRepoStructure  string `json:\"repo_structure\" type:\"text\" required:\"true\" default:\"OpenListTeam/OpenList\" help:\"structure:[path:]org/repo\"`\n\tShowReadme     bool   `json:\"show_readme\" type:\"bool\" default:\"true\" help:\"show README、LICENSE file\"`\n\tToken          string `json:\"token\" type:\"string\" required:\"false\" help:\"GitHub token, if you want to access private repositories or increase the rate limit\"`\n\tShowSourceCode bool   `json:\"show_source_code\" type:\"bool\" default:\"false\" help:\"show Source code (zip/tar.gz)\"`\n\tShowAllVersion bool   `json:\"show_all_version\" type:\"bool\" default:\"false\" help:\"show all versions\"`\n\tGitHubProxy    string `json:\"gh_proxy\" type:\"string\" default:\"\" help:\"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com \"`\n}\n\nvar config = driver.Config{\n\tName:     \"GitHub Releases\",\n\tNoUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &GithubReleases{}\n\t})\n}\n"
  },
  {
    "path": "drivers/github_releases/models.go",
    "content": "package github_releases\n\ntype Release struct {\n\tUrl             string    `json:\"url\"`\n\tAssetsUrl       string    `json:\"assets_url\"`\n\tUploadUrl       string    `json:\"upload_url\"`\n\tHtmlUrl         string    `json:\"html_url\"`\n\tId              int       `json:\"id\"`\n\tAuthor          User      `json:\"author\"`\n\tNodeId          string    `json:\"node_id\"`\n\tTagName         string    `json:\"tag_name\"`\n\tTargetCommitish string    `json:\"target_commitish\"`\n\tName            string    `json:\"name\"`\n\tDraft           bool      `json:\"draft\"`\n\tPrerelease      bool      `json:\"prerelease\"`\n\tCreatedAt       string    `json:\"created_at\"`\n\tPublishedAt     string    `json:\"published_at\"`\n\tAssets          []Asset   `json:\"assets\"`\n\tTarballUrl      string    `json:\"tarball_url\"`\n\tZipballUrl      string    `json:\"zipball_url\"`\n\tBody            string    `json:\"body\"`\n\tReactions       Reactions `json:\"reactions\"`\n}\n\ntype User struct {\n\tLogin             string `json:\"login\"`\n\tId                int    `json:\"id\"`\n\tNodeId            string `json:\"node_id\"`\n\tAvatarUrl         string `json:\"avatar_url\"`\n\tGravatarId        string `json:\"gravatar_id\"`\n\tUrl               string `json:\"url\"`\n\tHtmlUrl           string `json:\"html_url\"`\n\tFollowersUrl      string `json:\"followers_url\"`\n\tFollowingUrl      string `json:\"following_url\"`\n\tGistsUrl          string `json:\"gists_url\"`\n\tStarredUrl        string `json:\"starred_url\"`\n\tSubscriptionsUrl  string `json:\"subscriptions_url\"`\n\tOrganizationsUrl  string `json:\"organizations_url\"`\n\tReposUrl          string `json:\"repos_url\"`\n\tEventsUrl         string `json:\"events_url\"`\n\tReceivedEventsUrl string `json:\"received_events_url\"`\n\tType              string `json:\"type\"`\n\tUserViewType      string `json:\"user_view_type\"`\n\tSiteAdmin         bool   `json:\"site_admin\"`\n}\n\ntype Asset struct {\n\tUrl                string `json:\"url\"`\n\tId                 int    `json:\"id\"`\n\tNodeId             string `json:\"node_id\"`\n\tName               string `json:\"name\"`\n\tLabel              string `json:\"label\"`\n\tUploader           User   `json:\"uploader\"`\n\tContentType        string `json:\"content_type\"`\n\tState              string `json:\"state\"`\n\tSize               int64  `json:\"size\"`\n\tDownloadCount      int    `json:\"download_count\"`\n\tCreatedAt          string `json:\"created_at\"`\n\tUpdatedAt          string `json:\"updated_at\"`\n\tBrowserDownloadUrl string `json:\"browser_download_url\"`\n}\n\ntype Reactions struct {\n\tUrl        string `json:\"url\"`\n\tTotalCount int    `json:\"total_count\"`\n\tPlusOne    int    `json:\"+1\"`\n\tMinusOne   int    `json:\"-1\"`\n\tLaugh      int    `json:\"laugh\"`\n\tHooray     int    `json:\"hooray\"`\n\tConfused   int    `json:\"confused\"`\n\tHeart      int    `json:\"heart\"`\n\tRocket     int    `json:\"rocket\"`\n\tEyes       int    `json:\"eyes\"`\n}\n\ntype FileInfo struct {\n\tName        string `json:\"name\"`\n\tPath        string `json:\"path\"`\n\tSha         string `json:\"sha\"`\n\tSize        int64  `json:\"size\"`\n\tUrl         string `json:\"url\"`\n\tHtmlUrl     string `json:\"html_url\"`\n\tGitUrl      string `json:\"git_url\"`\n\tDownloadUrl string `json:\"download_url\"`\n\tType        string `json:\"type\"`\n}\n"
  },
  {
    "path": "drivers/github_releases/types.go",
    "content": "package github_releases\n\nimport (\n\t\"encoding/json\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype MountPoint struct {\n\tPoint     string      // 挂载点\n\tRepo      string      // 仓库名 owner/repo\n\tRelease   *Release    // Release 指针 latest\n\tReleases  *[]Release  // []Release 指针\n\tOtherFile *[]FileInfo // 仓库根目录下的其他文件\n}\n\n// 请求最新版本\nfunc (m *MountPoint) RequestRelease(get func(url string) (*resty.Response, error), refresh bool) {\n\tif m.Repo == \"\" {\n\t\treturn\n\t}\n\n\tif m.Release == nil || refresh {\n\t\tresp, _ := get(\"https://api.github.com/repos/\" + m.Repo + \"/releases/latest\")\n\t\tm.Release = new(Release)\n\t\tjson.Unmarshal(resp.Body(), m.Release)\n\t}\n}\n\n// 请求所有版本\nfunc (m *MountPoint) RequestReleases(get func(url string) (*resty.Response, error), refresh bool) {\n\tif m.Repo == \"\" {\n\t\treturn\n\t}\n\n\tif m.Releases == nil || refresh {\n\t\tresp, _ := get(\"https://api.github.com/repos/\" + m.Repo + \"/releases\")\n\t\tm.Releases = new([]Release)\n\t\tjson.Unmarshal(resp.Body(), m.Releases)\n\t}\n}\n\n// 获取最新版本\nfunc (m *MountPoint) GetLatestRelease() []File {\n\tfiles := make([]File, 0, len(m.Release.Assets))\n\tfor _, asset := range m.Release.Assets {\n\t\tfiles = append(files, File{\n\t\t\tPath:     path.Join(m.Point, asset.Name),\n\t\t\tFileName: asset.Name,\n\t\t\tSize:     asset.Size,\n\t\t\tType:     \"file\",\n\t\t\tUpdateAt: asset.UpdatedAt,\n\t\t\tCreateAt: asset.CreatedAt,\n\t\t\tUrl:      asset.BrowserDownloadUrl,\n\t\t})\n\t}\n\treturn files\n}\n\n// 获取最新版本大小\nfunc (m *MountPoint) GetLatestSize() int64 {\n\tsize := int64(0)\n\tfor _, asset := range m.Release.Assets {\n\t\tsize += asset.Size\n\t}\n\treturn size\n}\n\n// 获取所有版本\nfunc (m *MountPoint) GetAllVersion() []File {\n\tfiles := make([]File, 0)\n\tfor _, release := range *m.Releases {\n\t\tfile := File{\n\t\t\tPath:     path.Join(m.Point, release.TagName),\n\t\t\tFileName: release.TagName,\n\t\t\tSize:     m.GetSizeByTagName(release.TagName),\n\t\t\tType:     \"dir\",\n\t\t\tUpdateAt: release.PublishedAt,\n\t\t\tCreateAt: release.CreatedAt,\n\t\t\tUrl:      release.HtmlUrl,\n\t\t}\n\t\tfor _, asset := range release.Assets {\n\t\t\tfile.Size += asset.Size\n\t\t}\n\t\tfiles = append(files, file)\n\t}\n\treturn files\n}\n\n// 根据版本号获取版本\nfunc (m *MountPoint) GetReleaseByTagName(tagName string) []File {\n\tfor _, item := range *m.Releases {\n\t\tif item.TagName == tagName {\n\t\t\tfiles := make([]File, 0)\n\t\t\tfor _, asset := range item.Assets {\n\t\t\t\tfiles = append(files, File{\n\t\t\t\t\tPath:     path.Join(m.Point, tagName, asset.Name),\n\t\t\t\t\tFileName: asset.Name,\n\t\t\t\t\tSize:     asset.Size,\n\t\t\t\t\tType:     \"file\",\n\t\t\t\t\tUpdateAt: asset.UpdatedAt,\n\t\t\t\t\tCreateAt: asset.CreatedAt,\n\t\t\t\t\tUrl:      asset.BrowserDownloadUrl,\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn files\n\t\t}\n\t}\n\treturn nil\n}\n\n// 根据版本号获取版本大小\nfunc (m *MountPoint) GetSizeByTagName(tagName string) int64 {\n\tif m.Releases == nil {\n\t\treturn 0\n\t}\n\tfor _, item := range *m.Releases {\n\t\tif item.TagName == tagName {\n\t\t\tsize := int64(0)\n\t\t\tfor _, asset := range item.Assets {\n\t\t\t\tsize += asset.Size\n\t\t\t}\n\t\t\treturn size\n\t\t}\n\t}\n\treturn 0\n}\n\n// 获取所有版本大小\nfunc (m *MountPoint) GetAllVersionSize() int64 {\n\tif m.Releases == nil {\n\t\treturn 0\n\t}\n\tsize := int64(0)\n\tfor _, release := range *m.Releases {\n\t\tfor _, asset := range release.Assets {\n\t\t\tsize += asset.Size\n\t\t}\n\t}\n\treturn size\n}\n\nfunc (m *MountPoint) GetSourceCode() []File {\n\tfiles := make([]File, 0)\n\n\t// 无法获取文件大小，此处设为 1\n\tfiles = append(files, File{\n\t\tPath:     path.Join(m.Point, \"Source code (zip)\"),\n\t\tFileName: \"Source code (zip)\",\n\t\tSize:     1,\n\t\tType:     \"file\",\n\t\tUpdateAt: m.Release.CreatedAt,\n\t\tCreateAt: m.Release.CreatedAt,\n\t\tUrl:      m.Release.ZipballUrl,\n\t})\n\tfiles = append(files, File{\n\t\tPath:     path.Join(m.Point, \"Source code (tar.gz)\"),\n\t\tFileName: \"Source code (tar.gz)\",\n\t\tSize:     1,\n\t\tType:     \"file\",\n\t\tUpdateAt: m.Release.CreatedAt,\n\t\tCreateAt: m.Release.CreatedAt,\n\t\tUrl:      m.Release.TarballUrl,\n\t})\n\n\treturn files\n}\n\nfunc (m *MountPoint) GetSourceCodeByTagName(tagName string) []File {\n\tfor _, item := range *m.Releases {\n\t\tif item.TagName == tagName {\n\t\t\tfiles := make([]File, 0)\n\t\t\tfiles = append(files, File{\n\t\t\t\tPath:     path.Join(m.Point, \"Source code (zip)\"),\n\t\t\t\tFileName: \"Source code (zip)\",\n\t\t\t\tSize:     1,\n\t\t\t\tType:     \"file\",\n\t\t\t\tUpdateAt: item.CreatedAt,\n\t\t\t\tCreateAt: item.CreatedAt,\n\t\t\t\tUrl:      item.ZipballUrl,\n\t\t\t})\n\t\t\tfiles = append(files, File{\n\t\t\t\tPath:     path.Join(m.Point, \"Source code (tar.gz)\"),\n\t\t\t\tFileName: \"Source code (tar.gz)\",\n\t\t\t\tSize:     1,\n\t\t\t\tType:     \"file\",\n\t\t\t\tUpdateAt: item.CreatedAt,\n\t\t\t\tCreateAt: item.CreatedAt,\n\t\t\t\tUrl:      item.TarballUrl,\n\t\t\t})\n\t\t\treturn files\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *MountPoint) GetOtherFile(get func(url string) (*resty.Response, error), refresh bool) []File {\n\tif m.OtherFile == nil || refresh {\n\t\tresp, _ := get(\"https://api.github.com/repos/\" + m.Repo + \"/contents\")\n\t\tm.OtherFile = new([]FileInfo)\n\t\tjson.Unmarshal(resp.Body(), m.OtherFile)\n\t}\n\n\tfiles := make([]File, 0)\n\tdefaultTime := \"1970-01-01T00:00:00Z\"\n\tfor _, file := range *m.OtherFile {\n\t\tif strings.HasSuffix(file.Name, \".md\") || strings.HasPrefix(file.Name, \"LICENSE\") {\n\t\t\tfiles = append(files, File{\n\t\t\t\tPath:     path.Join(m.Point, file.Name),\n\t\t\t\tFileName: file.Name,\n\t\t\t\tSize:     file.Size,\n\t\t\t\tType:     \"file\",\n\t\t\t\tUpdateAt: defaultTime,\n\t\t\t\tCreateAt: defaultTime,\n\t\t\t\tUrl:      file.DownloadUrl,\n\t\t\t})\n\t\t}\n\t}\n\treturn files\n}\n\ntype File struct {\n\tPath     string // 文件路径\n\tFileName string // 文件名\n\tSize     int64  // 文件大小\n\tType     string // 文件类型\n\tUpdateAt string // 更新时间 eg:\"2025-01-27T16:10:16Z\"\n\tCreateAt string // 创建时间\n\tUrl      string // 下载链接\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f File) GetPath() string {\n\treturn f.Path\n}\n\nfunc (f File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) ModTime() time.Time {\n\tt, _ := time.Parse(time.RFC3339, f.CreateAt)\n\treturn t\n}\n\nfunc (f File) CreateTime() time.Time {\n\tt, _ := time.Parse(time.RFC3339, f.CreateAt)\n\treturn t\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == \"dir\"\n}\n\nfunc (f File) GetID() string {\n\treturn f.Url\n}\n"
  },
  {
    "path": "drivers/github_releases/util.go",
    "content": "package github_releases\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// 发送 GET 请求\nfunc (d *GithubReleases) GetRequest(url string) (*resty.Response, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Accept\", \"application/vnd.github+json\")\n\treq.SetHeader(\"X-GitHub-Api-Version\", \"2022-11-28\")\n\tif d.Addition.Token != \"\" {\n\t\treq.SetHeader(\"Authorization\", fmt.Sprintf(\"Bearer %s\", d.Addition.Token))\n\t}\n\tres, err := req.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() != 200 {\n\t\tlog.Warn(\"failed to get request: \", res.StatusCode(), res.String())\n\t}\n\treturn res, nil\n}\n\n// 解析挂载结构\nfunc (d *GithubReleases) ParseRepos(text string) ([]MountPoint, error) {\n\tlines := strings.Split(text, \"\\n\")\n\tpoints := make([]MountPoint, 0)\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tparts := strings.Split(line, \":\")\n\t\tpath, repo := \"\", \"\"\n\t\tif len(parts) == 1 {\n\t\t\tpath = \"/\"\n\t\t\trepo = parts[0]\n\t\t} else if len(parts) == 2 {\n\t\t\tpath = fmt.Sprintf(\"/%s\", strings.Trim(parts[0], \"/\"))\n\t\t\trepo = parts[1]\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"invalid format: %s\", line)\n\t\t}\n\n\t\tpoints = append(points, MountPoint{\n\t\t\tPoint:    path,\n\t\t\tRepo:     repo,\n\t\t\tRelease:  nil,\n\t\t\tReleases: nil,\n\t\t})\n\t}\n\td.points = points\n\treturn points, nil\n}\n\n// 获取下一级目录\nfunc GetNextDir(wholePath string, basePath string) string {\n\tbasePath = fmt.Sprintf(\"%s/\", strings.TrimRight(basePath, \"/\"))\n\tif !strings.HasPrefix(wholePath, basePath) {\n\t\treturn \"\"\n\t}\n\tremainingPath := strings.TrimLeft(strings.TrimPrefix(wholePath, basePath), \"/\")\n\tif remainingPath != \"\" {\n\t\tparts := strings.Split(remainingPath, \"/\")\n\t\tnextDir := parts[0]\n\t\tif strings.HasPrefix(wholePath, strings.TrimRight(basePath, \"/\")+\"/\"+nextDir) {\n\t\t\treturn nextDir\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// 判断当前目录是否是目标目录的祖先目录\nfunc IsAncestorDir(parentDir string, targetDir string) bool {\n\tabsTargetDir, _ := filepath.Abs(targetDir)\n\tabsParentDir, _ := filepath.Abs(parentDir)\n\treturn strings.HasPrefix(absTargetDir, absParentDir)\n}\n"
  },
  {
    "path": "drivers/google_drive/driver.go",
    "content": "package google_drive\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype GoogleDrive struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken            string\n\tServiceAccountFile     int\n\tServiceAccountFileList []string\n}\n\nfunc (d *GoogleDrive) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *GoogleDrive) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *GoogleDrive) Init(ctx context.Context) error {\n\tif d.ChunkSize == 0 {\n\t\td.ChunkSize = 5\n\t}\n\treturn d.refreshToken()\n}\n\nfunc (d *GoogleDrive) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *GoogleDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *GoogleDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\turl := fmt.Sprintf(\"https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true\", file.GetID())\n\t_, err := d.request(url, http.MethodGet, nil, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := model.Link{\n\t\tURL: url + \"&alt=media&acknowledgeAbuse=true\",\n\t\tHeader: http.Header{\n\t\t\t\"Authorization\": []string{\"Bearer \" + d.AccessToken},\n\t\t},\n\t}\n\treturn &link, nil\n}\n\nfunc (d *GoogleDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdata := base.Json{\n\t\t\"name\":     dirName,\n\t\t\"parents\":  []string{parentDir.GetID()},\n\t\t\"mimeType\": \"application/vnd.google-apps.folder\",\n\t}\n\t_, err := d.request(\"https://www.googleapis.com/drive/v3/files\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *GoogleDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tquery := map[string]string{\n\t\t\"addParents\":    dstDir.GetID(),\n\t\t\"removeParents\": \"root\",\n\t}\n\turl := \"https://www.googleapis.com/drive/v3/files/\" + srcObj.GetID()\n\t_, err := d.request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *GoogleDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tdata := base.Json{\n\t\t\"name\": newName,\n\t}\n\turl := \"https://www.googleapis.com/drive/v3/files/\" + srcObj.GetID()\n\t_, err := d.request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *GoogleDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error {\n\turl := \"https://www.googleapis.com/drive/v3/files/\" + obj.GetID()\n\t_, err := d.request(url, http.MethodDelete, nil, nil)\n\treturn err\n}\n\nfunc (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tobj := stream.GetExist()\n\tvar (\n\t\te    Error\n\t\turl  string\n\t\tdata base.Json\n\t\tres  *resty.Response\n\t\terr  error\n\t)\n\tif obj != nil {\n\t\turl = fmt.Sprintf(\"https://www.googleapis.com/upload/drive/v3/files/%s?uploadType=resumable&supportsAllDrives=true\", obj.GetID())\n\t\tdata = base.Json{}\n\t} else {\n\t\tdata = base.Json{\n\t\t\t\"name\":    stream.GetName(),\n\t\t\t\"parents\": []string{dstDir.GetID()},\n\t\t}\n\t\turl = \"https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true\"\n\t}\n\treq := base.NoRedirectClient.R().\n\t\tSetHeaders(map[string]string{\n\t\t\t\"Authorization\":           \"Bearer \" + d.AccessToken,\n\t\t\t\"X-Upload-Content-Type\":   stream.GetMimetype(),\n\t\t\t\"X-Upload-Content-Length\": strconv.FormatInt(stream.GetSize(), 10),\n\t\t}).\n\t\tSetError(&e).SetBody(data).SetContext(ctx)\n\tif obj != nil {\n\t\tres, err = req.Patch(url)\n\t} else {\n\t\tres, err = req.Post(url)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error.Code != 0 {\n\t\tif e.Error.Code == 401 {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn d.Put(ctx, dstDir, stream, up)\n\t\t}\n\t\treturn fmt.Errorf(\"%s: %v\", e.Error.Message, e.Error.Errors)\n\t}\n\tputUrl := res.Header().Get(\"location\")\n\tif stream.GetSize() < d.ChunkSize*1024*1024 {\n\t\t_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {\n\t\t\treq.SetHeader(\"Content-Length\", strconv.FormatInt(stream.GetSize(), 10)).\n\t\t\t\tSetBody(driver.NewLimitedUploadStream(ctx, stream))\n\t\t}, nil)\n\t} else {\n\t\terr = d.chunkUpload(ctx, stream, putUrl, up)\n\t}\n\treturn err\n}\n\nfunc (d *GoogleDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif d.DisableDiskUsage {\n\t\treturn nil, errs.NotImplement\n\t}\n\tabout, err := d.getAbout(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar total, used int64\n\tif about.StorageQuota.Limit == nil {\n\t\ttotal = 0\n\t} else {\n\t\ttotal, err = strconv.ParseInt(*about.StorageQuota.Limit, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tused, err = strconv.ParseInt(about.StorageQuota.Usage, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*GoogleDrive)(nil)\n"
  },
  {
    "path": "drivers/google_drive/meta.go",
    "content": "package google_drive\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRefreshToken     string `json:\"refresh_token\" required:\"true\"`\n\tOrderBy          string `json:\"order_by\" type:\"string\" help:\"such as: folder,name,modifiedTime\"`\n\tOrderDirection   string `json:\"order_direction\" type:\"select\" options:\"asc,desc\"`\n\tUseOnlineAPI     bool   `json:\"use_online_api\" default:\"true\"`\n\tAPIAddress       string `json:\"api_url_address\" default:\"https://api.oplist.org/googleui/renewapi\"`\n\tClientID         string `json:\"client_id\"`\n\tClientSecret     string `json:\"client_secret\"`\n\tChunkSize        int64  `json:\"chunk_size\" type:\"number\" default:\"5\" help:\"chunk size while uploading (unit: MB)\"`\n\tDisableDiskUsage bool   `json:\"disable_disk_usage\" default:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:        \"GoogleDrive\",\n\tOnlyProxy:   true,\n\tDefaultRoot: \"root\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &GoogleDrive{}\n\t})\n}\n"
  },
  {
    "path": "drivers/google_drive/types.go",
    "content": "package google_drive\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype TokenError struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\ntype Files struct {\n\tNextPageToken string `json:\"nextPageToken\"`\n\tFiles         []File `json:\"files\"`\n}\n\ntype File struct {\n\tId              string    `json:\"id\"`\n\tName            string    `json:\"name\"`\n\tMimeType        string    `json:\"mimeType\"`\n\tModifiedTime    time.Time `json:\"modifiedTime\"`\n\tCreatedTime     time.Time `json:\"createdTime\"`\n\tSize            string    `json:\"size\"`\n\tThumbnailLink   string    `json:\"thumbnailLink\"`\n\tShortcutDetails struct {\n\t\tTargetId       string `json:\"targetId\"`\n\t\tTargetMimeType string `json:\"targetMimeType\"`\n\t} `json:\"shortcutDetails\"`\n\n\tMD5Checksum    string `json:\"md5Checksum\"`\n\tSHA1Checksum   string `json:\"sha1Checksum\"`\n\tSHA256Checksum string `json:\"sha256Checksum\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\tlog.Debugf(\"google file: %+v\", f)\n\tsize, _ := strconv.ParseInt(f.Size, 10, 64)\n\tobj := &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Id,\n\t\t\tName:     f.Name,\n\t\t\tSize:     size,\n\t\t\tCtime:    f.CreatedTime,\n\t\t\tModified: f.ModifiedTime,\n\t\t\tIsFolder: f.MimeType == \"application/vnd.google-apps.folder\",\n\t\t\tHashInfo: utils.NewHashInfoByMap(map[*utils.HashType]string{\n\t\t\t\tutils.MD5:    f.MD5Checksum,\n\t\t\t\tutils.SHA1:   f.SHA1Checksum,\n\t\t\t\tutils.SHA256: f.SHA256Checksum,\n\t\t\t}),\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: f.ThumbnailLink,\n\t\t},\n\t}\n\tif f.MimeType == \"application/vnd.google-apps.shortcut\" {\n\t\tobj.ID = f.ShortcutDetails.TargetId\n\t\tobj.IsFolder = f.ShortcutDetails.TargetMimeType == \"application/vnd.google-apps.folder\"\n\t}\n\treturn obj\n}\n\ntype Error struct {\n\tError struct {\n\t\tErrors []struct {\n\t\t\tDomain       string `json:\"domain\"`\n\t\t\tReason       string `json:\"reason\"`\n\t\t\tMessage      string `json:\"message\"`\n\t\t\tLocationType string `json:\"location_type\"`\n\t\t\tLocation     string `json:\"location\"`\n\t\t}\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\ntype AboutResp struct {\n\tStorageQuota struct {\n\t\tLimit             *string `json:\"limit\"`\n\t\tUsage             string  `json:\"usage\"`\n\t\tUsageInDrive      string  `json:\"usageInDrive\"`\n\t\tUsageInDriveTrash string  `json:\"usageInDriveTrash\"`\n\t}\n}\n"
  },
  {
    "path": "drivers/google_drive/util.go",
    "content": "package google_drive\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/avast/retry-go\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/golang-jwt/jwt/v4\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\n// Google Drive API field constants\nconst (\n\t// File list query fields\n\tFilesListFields = \"files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken\"\n\t// Single file query fields\n\tFileInfoFields = \"id,name,mimeType,size,md5Checksum,sha1Checksum,sha256Checksum\"\n)\n\ntype googleDriveServiceAccount struct {\n\t// Type                    string `json:\"type\"`\n\t// ProjectID               string `json:\"project_id\"`\n\t// PrivateKeyID            string `json:\"private_key_id\"`\n\tPrivateKey  string `json:\"private_key\"`\n\tClientEMail string `json:\"client_email\"`\n\t// ClientID                string `json:\"client_id\"`\n\t// AuthURI                 string `json:\"auth_uri\"`\n\tTokenURI string `json:\"token_uri\"`\n\t// AuthProviderX509CertURL string `json:\"auth_provider_x509_cert_url\"`\n\t// ClientX509CertURL       string `json:\"client_x509_cert_url\"`\n}\n\nfunc (d *GoogleDrive) refreshToken() error {\n\t// 使用在线API刷新Token，无需ClientID和ClientSecret\n\tif d.UseOnlineAPI && len(d.APIAddress) > 0 {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"googleui_go\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\td.RefreshToken = resp.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\t// 使用本地客户端的情况下检查是否为空\n\tif d.ClientID == \"\" || d.ClientSecret == \"\" {\n\t\treturn fmt.Errorf(\"empty ClientID or ClientSecret\")\n\t}\n\t// 走原有的刷新逻辑\n\n\t// googleDriveServiceAccountFile gdsaFile\n\tgdsaFile, gdsaFileErr := os.Stat(d.RefreshToken)\n\tif gdsaFileErr == nil {\n\t\tgdsaFileThis := d.RefreshToken\n\t\tif gdsaFile.IsDir() {\n\t\t\tif len(d.ServiceAccountFileList) <= 0 {\n\t\t\t\tgdsaReadDir, gdsaDirErr := os.ReadDir(d.RefreshToken)\n\t\t\t\tif gdsaDirErr != nil {\n\t\t\t\t\tlog.Error(\"read dir fail\")\n\t\t\t\t\treturn gdsaDirErr\n\t\t\t\t}\n\t\t\t\tvar gdsaFileList []string\n\t\t\t\tfor _, fi := range gdsaReadDir {\n\t\t\t\t\tif !fi.IsDir() {\n\t\t\t\t\t\tmatch, _ := regexp.MatchString(\"^.*\\\\.json$\", fi.Name())\n\t\t\t\t\t\tif !match {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgdsaDirText := d.RefreshToken\n\t\t\t\t\t\tif d.RefreshToken[len(d.RefreshToken)-1:] != \"/\" {\n\t\t\t\t\t\t\tgdsaDirText = d.RefreshToken + \"/\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgdsaFileList = append(gdsaFileList, gdsaDirText+fi.Name())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\td.ServiceAccountFileList = gdsaFileList\n\t\t\t\tgdsaFileThis = d.ServiceAccountFileList[d.ServiceAccountFile]\n\t\t\t\td.ServiceAccountFile++\n\t\t\t} else {\n\t\t\t\tif d.ServiceAccountFile < len(d.ServiceAccountFileList) {\n\t\t\t\t\td.ServiceAccountFile++\n\t\t\t\t} else {\n\t\t\t\t\td.ServiceAccountFile = 0\n\t\t\t\t}\n\t\t\t\tgdsaFileThis = d.ServiceAccountFileList[d.ServiceAccountFile]\n\t\t\t}\n\t\t}\n\n\t\tgdsaFileThisContent, err := os.ReadFile(gdsaFileThis)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Now let's unmarshal the data into `payload`\n\t\tvar jsonData googleDriveServiceAccount\n\t\terr = utils.Json.Unmarshal(gdsaFileThisContent, &jsonData)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tgdsaScope := \"https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.metadata https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.scripts\"\n\n\t\ttimeNow := time.Now()\n\t\tvar timeStart int64 = timeNow.Unix()\n\t\tvar timeEnd int64 = timeNow.Add(time.Minute * 60).Unix()\n\n\t\t// load private key from string\n\t\tprivateKeyPem, _ := pem.Decode([]byte(jsonData.PrivateKey))\n\t\tprivateKey, _ := x509.ParsePKCS8PrivateKey(privateKeyPem.Bytes)\n\n\t\tjwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256,\n\t\t\tjwt.MapClaims{\n\t\t\t\t\"iss\":   jsonData.ClientEMail,\n\t\t\t\t\"scope\": gdsaScope,\n\t\t\t\t\"aud\":   jsonData.TokenURI,\n\t\t\t\t\"exp\":   timeEnd,\n\t\t\t\t\"iat\":   timeStart,\n\t\t\t})\n\t\tassertion, err := jwtToken.SignedString(privateKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar resp base.TokenResp\n\t\tvar e TokenError\n\t\tres, err := base.RestyClient.R().SetResult(&resp).SetError(&e).\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"assertion\":  assertion,\n\t\t\t\t\"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n\t\t\t}).Post(jsonData.TokenURI)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debug(res.String())\n\t\tif e.Error != \"\" {\n\t\t\treturn fmt.Errorf(e.Error)\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\treturn nil\n\t} else if os.IsExist(gdsaFileErr) {\n\t\treturn gdsaFileErr\n\t}\n\turl := \"https://www.googleapis.com/oauth2/v4/token\"\n\tvar resp base.TokenResp\n\tvar e TokenError\n\tres, err := base.RestyClient.R().SetResult(&resp).SetError(&e).\n\t\tSetFormData(map[string]string{\n\t\t\t\"client_id\":     d.ClientID,\n\t\t\t\"client_secret\": d.ClientSecret,\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t}).Post(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debug(res.String())\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(e.Error)\n\t}\n\td.AccessToken = resp.AccessToken\n\treturn nil\n}\n\nfunc (d *GoogleDrive) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\treq.SetQueryParam(\"includeItemsFromAllDrives\", \"true\")\n\treq.SetQueryParam(\"supportsAllDrives\", \"true\")\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Error\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Error.Code != 0 {\n\t\tif e.Error.Code == 401 {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(url, method, callback, resp)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%s: %v\", e.Error.Message, e.Error.Errors)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *GoogleDrive) getFiles(id string) ([]File, error) {\n\tpageToken := \"first\"\n\tres := make([]File, 0)\n\tfor pageToken != \"\" {\n\t\tif pageToken == \"first\" {\n\t\t\tpageToken = \"\"\n\t\t}\n\t\tvar resp Files\n\t\torderBy := \"folder,name,modifiedTime desc\"\n\t\tif d.OrderBy != \"\" {\n\t\t\torderBy = d.OrderBy + \" \" + d.OrderDirection\n\t\t}\n\t\tquery := map[string]string{\n\t\t\t\"orderBy\":  orderBy,\n\t\t\t\"fields\":   FilesListFields,\n\t\t\t\"pageSize\": \"1000\",\n\t\t\t\"q\":        fmt.Sprintf(\"'%s' in parents and trashed = false\", id),\n\t\t\t//\"includeItemsFromAllDrives\": \"true\",\n\t\t\t//\"supportsAllDrives\":         \"true\",\n\t\t\t\"pageToken\": pageToken,\n\t\t}\n\t\t_, err := d.request(\"https://www.googleapis.com/drive/v3/files\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpageToken = resp.NextPageToken\n\n\t\t// Batch process shortcuts, API calls only for file shortcuts\n\t\tshortcutTargetIds := make([]string, 0)\n\t\tshortcutIndices := make([]int, 0)\n\n\t\t// Collect target IDs of all file shortcuts (skip folder shortcuts)\n\t\tfor i := range resp.Files {\n\t\t\tif resp.Files[i].MimeType == \"application/vnd.google-apps.shortcut\" &&\n\t\t\t\tresp.Files[i].ShortcutDetails.TargetId != \"\" &&\n\t\t\t\tresp.Files[i].ShortcutDetails.TargetMimeType != \"application/vnd.google-apps.folder\" {\n\t\t\t\tshortcutTargetIds = append(shortcutTargetIds, resp.Files[i].ShortcutDetails.TargetId)\n\t\t\t\tshortcutIndices = append(shortcutIndices, i)\n\t\t\t}\n\t\t}\n\n\t\t// Batch get target file info (only for file shortcuts)\n\t\tif len(shortcutTargetIds) > 0 {\n\t\t\ttargetFiles := d.batchGetTargetFilesInfo(shortcutTargetIds)\n\t\t\t// Update shortcut file info\n\t\t\tfor j, targetId := range shortcutTargetIds {\n\t\t\t\tif targetFile, exists := targetFiles[targetId]; exists {\n\t\t\t\t\tfileIndex := shortcutIndices[j]\n\t\t\t\t\tif targetFile.Size != \"\" {\n\t\t\t\t\t\tresp.Files[fileIndex].Size = targetFile.Size\n\t\t\t\t\t}\n\t\t\t\t\tif targetFile.MD5Checksum != \"\" {\n\t\t\t\t\t\tresp.Files[fileIndex].MD5Checksum = targetFile.MD5Checksum\n\t\t\t\t\t}\n\t\t\t\t\tif targetFile.SHA1Checksum != \"\" {\n\t\t\t\t\t\tresp.Files[fileIndex].SHA1Checksum = targetFile.SHA1Checksum\n\t\t\t\t\t}\n\t\t\t\t\tif targetFile.SHA256Checksum != \"\" {\n\t\t\t\t\t\tresp.Files[fileIndex].SHA256Checksum = targetFile.SHA256Checksum\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tres = append(res, resp.Files...)\n\t}\n\treturn res, nil\n}\n\n// getTargetFileInfo gets target file details for shortcuts\nfunc (d *GoogleDrive) getTargetFileInfo(targetId string) (File, error) {\n\tvar targetFile File\n\turl := fmt.Sprintf(\"https://www.googleapis.com/drive/v3/files/%s\", targetId)\n\tquery := map[string]string{\n\t\t\"fields\": FileInfoFields,\n\t}\n\t_, err := d.request(url, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &targetFile)\n\tif err != nil {\n\t\treturn File{}, err\n\t}\n\treturn targetFile, nil\n}\n\n// batchGetTargetFilesInfo batch gets target file info, sequential processing to avoid concurrency complexity\nfunc (d *GoogleDrive) batchGetTargetFilesInfo(targetIds []string) map[string]File {\n\tif len(targetIds) == 0 {\n\t\treturn make(map[string]File)\n\t}\n\n\tresult := make(map[string]File)\n\t// Sequential processing to avoid concurrency complexity\n\tfor _, targetId := range targetIds {\n\t\tfile, err := d.getTargetFileInfo(targetId)\n\t\tif err == nil {\n\t\t\tresult[targetId] = file\n\t\t}\n\t}\n\treturn result\n}\n\nfunc (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer, url string, up driver.UpdateProgress) error {\n\tdefaultChunkSize := d.ChunkSize * 1024 * 1024\n\tss, err := stream.NewStreamSectionReader(file, int(defaultChunkSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar offset int64 = 0\n\turl += \"?includeItemsFromAllDrives=true&supportsAllDrives=true\"\n\tfor offset < file.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tchunkSize := min(file.GetSize()-offset, defaultChunkSize)\n\t\treader, err := ss.GetSectionReader(offset, chunkSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlimitedReader := driver.NewLimitedUploadStream(ctx, reader)\n\t\terr = retry.Do(func() error {\n\t\t\treader.Seek(0, io.SeekStart)\n\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, limitedReader)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treq.Header = map[string][]string{\n\t\t\t\t\"Authorization\":  {\"Bearer \" + d.AccessToken},\n\t\t\t\t\"Content-Length\": {strconv.FormatInt(chunkSize, 10)},\n\t\t\t\t\"Content-Range\":  {fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+chunkSize-1, file.GetSize())},\n\t\t\t}\n\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer res.Body.Close()\n\t\t\tbytes, _ := io.ReadAll(res.Body)\n\t\t\tvar e Error\n\t\t\tutils.Json.Unmarshal(bytes, &e)\n\t\t\tif e.Error.Code != 0 {\n\t\t\t\tif e.Error.Code == 401 {\n\t\t\t\t\terr = d.refreshToken()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"%s: %v\", e.Error.Message, e.Error.Errors)\n\t\t\t}\n\t\t\tup(float64(offset+chunkSize) / float64(file.GetSize()) * 100)\n\t\t\treturn nil\n\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second))\n\t\tss.FreeSectionReader(reader)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\toffset += chunkSize\n\t}\n\treturn nil\n}\n\nfunc (d *GoogleDrive) getAbout(ctx context.Context) (*AboutResp, error) {\n\tquery := map[string]string{\n\t\t\"fields\": \"storageQuota\",\n\t}\n\tvar resp AboutResp\n\t_, err := d.request(\"https://www.googleapis.com/drive/v3/about\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/google_photo/driver.go",
    "content": "package google_photo\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype GooglePhoto struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n}\n\nfunc (d *GooglePhoto) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *GooglePhoto) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *GooglePhoto) Init(ctx context.Context) error {\n\treturn d.refreshToken()\n}\n\nfunc (d *GooglePhoto) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *GooglePhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src MediaItem) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf, err := d.getMedia(file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif strings.Contains(f.MimeType, \"image/\") {\n\t\treturn &model.Link{\n\t\t\tURL: f.BaseURL + \"=d\",\n\t\t}, nil\n\t} else if strings.Contains(f.MimeType, \"video/\") {\n\t\treturn &model.Link{\n\t\t\tURL: f.BaseURL + \"=dv\",\n\t\t}, nil\n\t}\n\treturn &model.Link{}, nil\n}\n\nfunc (d *GooglePhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GooglePhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GooglePhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GooglePhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GooglePhoto) Remove(ctx context.Context, obj model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *GooglePhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tvar e Error\n\t// Create resumable upload url\n\tpostHeaders := map[string]string{\n\t\t\"Authorization\":              \"Bearer \" + d.AccessToken,\n\t\t\"Content-type\":               \"application/octet-stream\",\n\t\t\"X-Goog-Upload-Command\":      \"start\",\n\t\t\"X-Goog-Upload-Content-Type\": stream.GetMimetype(),\n\t\t\"X-Goog-Upload-Protocol\":     \"resumable\",\n\t\t\"X-Goog-Upload-Raw-Size\":     strconv.FormatInt(stream.GetSize(), 10),\n\t}\n\turl := \"https://photoslibrary.googleapis.com/v1/uploads\"\n\tres, err := base.NoRedirectClient.R().SetHeaders(postHeaders).\n\t\tSetError(&e).\n\t\tPost(url)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error.Code != 0 {\n\t\tif e.Error.Code == 401 {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn d.Put(ctx, dstDir, stream, up)\n\t\t}\n\t\treturn fmt.Errorf(\"%s: %v\", e.Error.Message, e.Error.Errors)\n\t}\n\n\t//Upload to the Google Photo\n\tpostUrl := res.Header().Get(\"X-Goog-Upload-URL\")\n\t//chunkSize := res.Header().Get(\"X-Goog-Upload-Chunk-Granularity\")\n\tpostHeaders = map[string]string{\n\t\t\"X-Goog-Upload-Command\": \"upload, finalize\",\n\t\t\"X-Goog-Upload-Offset\":  \"0\",\n\t}\n\n\tresp, err := d.request(postUrl, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, stream)).SetContext(ctx)\n\t}, nil, postHeaders)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\t//Create MediaItem\n\tcreateItemUrl := \"https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate\"\n\n\tpostHeaders = map[string]string{\n\t\t\"X-Goog-Upload-Command\": \"upload, finalize\",\n\t\t\"X-Goog-Upload-Offset\":  \"0\",\n\t}\n\n\tdata := base.Json{\n\t\t\"newMediaItems\": []base.Json{\n\t\t\t{\n\t\t\t\t\"description\": \"item-description\",\n\t\t\t\t\"simpleMediaItem\": base.Json{\n\t\t\t\t\t\"fileName\":    stream.GetName(),\n\t\t\t\t\t\"uploadToken\": string(resp),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err = d.request(createItemUrl, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil, postHeaders)\n\n\treturn err\n}\n\nvar _ driver.Driver = (*GooglePhoto)(nil)\n"
  },
  {
    "path": "drivers/google_photo/meta.go",
    "content": "package google_photo\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\n\tClientID     string `json:\"client_id\" required:\"true\" default:\"202264815644.apps.googleusercontent.com\"`\n\tClientSecret string `json:\"client_secret\" required:\"true\" default:\"X4Z3ca8xfWDb1Voo-F9a7ZxJ\"`\n\tShowArchive  bool   `json:\"show_archive\"`\n}\n\nvar config = driver.Config{\n\tName:        \"GooglePhoto\",\n\tOnlyProxy:   true,\n\tDefaultRoot: \"root\",\n\tNoUpload:    true,\n\tLocalSort:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &GooglePhoto{}\n\t})\n}\n"
  },
  {
    "path": "drivers/google_photo/types.go",
    "content": "package google_photo\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype TokenError struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\ntype Items struct {\n\tNextPageToken string      `json:\"nextPageToken\"`\n\tMediaItems    []MediaItem `json:\"mediaItems,omitempty\"`\n\tAlbums        []MediaItem `json:\"albums,omitempty\"`\n\tSharedAlbums  []MediaItem `json:\"sharedAlbums,omitempty\"`\n}\n\ntype MediaItem struct {\n\tId                string        `json:\"id\"`\n\tTitle             string        `json:\"title,omitempty\"`\n\tBaseURL           string        `json:\"baseUrl,omitempty\"`\n\tCoverPhotoBaseUrl string        `json:\"coverPhotoBaseUrl,omitempty\"`\n\tMimeType          string        `json:\"mimeType,omitempty\"`\n\tFileName          string        `json:\"filename,omitempty\"`\n\tMediaMetadata     MediaMetadata `json:\"mediaMetadata,omitempty\"`\n}\n\ntype MediaMetadata struct {\n\tCreationTime time.Time `json:\"creationTime\"`\n\tWidth        string    `json:\"width\"`\n\tHeight       string    `json:\"height\"`\n\tPhoto        Photo     `json:\"photo,omitempty\"`\n\tVideo        Video     `json:\"video,omitempty\"`\n}\n\ntype Photo struct {\n}\n\ntype Video struct {\n}\n\nfunc fileToObj(f MediaItem) *model.ObjThumb {\n\tif !reflect.DeepEqual(f.MediaMetadata, MediaMetadata{}) {\n\t\treturn &model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       f.Id,\n\t\t\t\tName:     f.FileName,\n\t\t\t\tSize:     0,\n\t\t\t\tModified: f.MediaMetadata.CreationTime,\n\t\t\t\tIsFolder: false,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\tThumbnail: f.BaseURL + \"=w100-h100-c\",\n\t\t\t},\n\t\t}\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Id,\n\t\t\tName:     f.Title,\n\t\t\tSize:     0,\n\t\t\tModified: time.Time{},\n\t\t\tIsFolder: true,\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}\n}\n\ntype Error struct {\n\tError struct {\n\t\tErrors []struct {\n\t\t\tDomain       string `json:\"domain\"`\n\t\t\tReason       string `json:\"reason\"`\n\t\t\tMessage      string `json:\"message\"`\n\t\t\tLocationType string `json:\"location_type\"`\n\t\t\tLocation     string `json:\"location\"`\n\t\t}\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n"
  },
  {
    "path": "drivers/google_photo/util.go",
    "content": "package google_photo\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\n// do others that not defined in Driver interface\n\nconst (\n\tFETCH_ALL          = \"all\"\n\tFETCH_ALBUMS       = \"albums\"\n\tFETCH_ROOT         = \"root\"\n\tFETCH_SHARE_ALBUMS = \"share_albums\"\n)\n\nfunc (d *GooglePhoto) refreshToken() error {\n\turl := \"https://www.googleapis.com/oauth2/v4/token\"\n\tvar resp base.TokenResp\n\tvar e TokenError\n\t_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).\n\t\tSetFormData(map[string]string{\n\t\t\t\"client_id\":     d.ClientID,\n\t\t\t\"client_secret\": d.ClientSecret,\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t}).Post(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(e.Error)\n\t}\n\td.AccessToken = resp.AccessToken\n\treturn nil\n}\n\nfunc (d *GooglePhoto) request(url string, method string, callback base.ReqCallback, resp interface{}, headers map[string]string) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\treq.SetHeader(\"Accept-Encoding\", \"gzip\")\n\tif headers != nil {\n\t\treq.SetHeaders(headers)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Error\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Error.Code != 0 {\n\t\tif e.Error.Code == 401 {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(url, method, callback, resp, headers)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%s: %v\", e.Error.Message, e.Error.Errors)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *GooglePhoto) getFiles(id string) ([]MediaItem, error) {\n\tswitch id {\n\tcase FETCH_ALL:\n\t\treturn d.getAllMedias()\n\tcase FETCH_ALBUMS:\n\t\treturn d.getAlbums()\n\tcase FETCH_SHARE_ALBUMS:\n\t\treturn d.getShareAlbums()\n\tcase FETCH_ROOT:\n\t\treturn d.getFakeRoot()\n\tdefault:\n\t\treturn d.getMedias(id)\n\t}\n}\n\nfunc (d *GooglePhoto) getFakeRoot() ([]MediaItem, error) {\n\treturn []MediaItem{\n\t\t{\n\t\t\tId:    FETCH_ALL,\n\t\t\tTitle: FETCH_ALL,\n\t\t},\n\t\t{\n\t\t\tId:    FETCH_ALBUMS,\n\t\t\tTitle: FETCH_ALBUMS,\n\t\t},\n\t\t{\n\t\t\tId:    FETCH_SHARE_ALBUMS,\n\t\t\tTitle: FETCH_SHARE_ALBUMS,\n\t\t},\n\t}, nil\n}\n\nfunc (d *GooglePhoto) getAlbums() ([]MediaItem, error) {\n\treturn d.fetchItems(\n\t\t\"https://photoslibrary.googleapis.com/v1/albums\",\n\t\tmap[string]string{\n\t\t\t\"fields\":    \"albums(id,title,coverPhotoBaseUrl),nextPageToken\",\n\t\t\t\"pageSize\":  \"50\",\n\t\t\t\"pageToken\": \"first\",\n\t\t},\n\t\thttp.MethodGet)\n}\n\nfunc (d *GooglePhoto) getShareAlbums() ([]MediaItem, error) {\n\treturn d.fetchItems(\n\t\t\"https://photoslibrary.googleapis.com/v1/sharedAlbums\",\n\t\tmap[string]string{\n\t\t\t\"fields\":    \"sharedAlbums(id,title,coverPhotoBaseUrl),nextPageToken\",\n\t\t\t\"pageSize\":  \"50\",\n\t\t\t\"pageToken\": \"first\",\n\t\t},\n\t\thttp.MethodGet)\n}\n\nfunc (d *GooglePhoto) getMedias(albumId string) ([]MediaItem, error) {\n\treturn d.fetchItems(\n\t\t\"https://photoslibrary.googleapis.com/v1/mediaItems:search\",\n\t\tmap[string]string{\n\t\t\t\"fields\":    \"mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken\",\n\t\t\t\"pageSize\":  \"100\",\n\t\t\t\"albumId\":   albumId,\n\t\t\t\"pageToken\": \"first\",\n\t\t}, http.MethodPost)\n}\n\nfunc (d *GooglePhoto) getAllMedias() ([]MediaItem, error) {\n\treturn d.fetchItems(\n\t\t\"https://photoslibrary.googleapis.com/v1/mediaItems\",\n\t\tmap[string]string{\n\t\t\t\"fields\":    \"mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken\",\n\t\t\t\"pageSize\":  \"100\",\n\t\t\t\"pageToken\": \"first\",\n\t\t},\n\t\thttp.MethodGet)\n}\n\nfunc (d *GooglePhoto) getMedia(id string) (MediaItem, error) {\n\tvar resp MediaItem\n\n\tquery := map[string]string{\n\t\t\"fields\": \"mediaMetadata,baseUrl,mimeType\",\n\t}\n\t_, err := d.request(fmt.Sprintf(\"https://photoslibrary.googleapis.com/v1/mediaItems/%s\", id), http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp, nil)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, nil\n}\n\nfunc (d *GooglePhoto) fetchItems(url string, query map[string]string, method string) ([]MediaItem, error) {\n\tres := make([]MediaItem, 0)\n\tfor query[\"pageToken\"] != \"\" {\n\t\tif query[\"pageToken\"] == \"first\" {\n\t\t\tquery[\"pageToken\"] = \"\"\n\t\t}\n\t\tvar resp Items\n\n\t\t_, err := d.request(url, method, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tquery[\"pageToken\"] = resp.NextPageToken\n\t\tres = append(res, resp.MediaItems...)\n\t\tres = append(res, resp.Albums...)\n\t\tres = append(res, resp.SharedAlbums...)\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "drivers/halalcloud/driver.go",
    "content": "package halalcloud\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/city404/v6-public-rpc-proto/go/v6/common\"\n\tpbPublicUser \"github.com/city404/v6-public-rpc-proto/go/v6/user\"\n\tpubUserFile \"github.com/city404/v6-public-rpc-proto/go/v6/userfile\"\n\t\"github.com/rclone/rclone/lib/readers\"\n\t\"github.com/zzzhr1990/go-common-entity/userfile\"\n)\n\ntype HalalCloud struct {\n\t*HalalCommon\n\tmodel.Storage\n\tAddition\n\n\tuploadThread int\n}\n\nfunc (d *HalalCloud) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *HalalCloud) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *HalalCloud) Init(ctx context.Context) error {\n\td.uploadThread, _ = strconv.Atoi(d.UploadThread)\n\tif d.uploadThread < 1 || d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 3, \"3\"\n\t}\n\n\tif d.HalalCommon == nil {\n\t\td.HalalCommon = &HalalCommon{\n\t\t\tCommon: &Common{},\n\t\t\tAuthService: &AuthService{\n\t\t\t\tappID: func() string {\n\t\t\t\t\tif d.Addition.AppID != \"\" {\n\t\t\t\t\t\treturn d.Addition.AppID\n\t\t\t\t\t}\n\t\t\t\t\treturn AppID\n\t\t\t\t}(),\n\t\t\t\tappVersion: func() string {\n\t\t\t\t\tif d.Addition.AppVersion != \"\" {\n\t\t\t\t\t\treturn d.Addition.AppVersion\n\t\t\t\t\t}\n\t\t\t\t\treturn AppVersion\n\t\t\t\t}(),\n\t\t\t\tappSecret: func() string {\n\t\t\t\t\tif d.Addition.AppSecret != \"\" {\n\t\t\t\t\t\treturn d.Addition.AppSecret\n\t\t\t\t\t}\n\t\t\t\t\treturn AppSecret\n\t\t\t\t}(),\n\t\t\t\ttr: &TokenResp{\n\t\t\t\t\tRefreshToken: d.Addition.RefreshToken,\n\t\t\t\t},\n\t\t\t},\n\t\t\tUserInfo: &UserInfo{},\n\t\t\trefreshTokenFunc: func(token string) error {\n\t\t\t\td.Addition.RefreshToken = token\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\t}\n\n\t// 防止重复登录\n\tif d.Addition.RefreshToken == \"\" || !d.IsLogin() {\n\t\tas, err := d.NewAuthServiceWithOauth()\n\t\tif err != nil {\n\t\t\td.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\treturn err\n\t\t}\n\t\td.HalalCommon.AuthService = as\n\t\td.SetTokenResp(as.tr)\n\t\top.MustSaveDriverStorage(d)\n\t}\n\tvar err error\n\td.HalalCommon.serv, err = d.NewAuthService(d.Addition.RefreshToken)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *HalalCloud) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *HalalCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.getFiles(ctx, dir)\n}\n\nfunc (d *HalalCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\treturn d.getLink(ctx, file, args)\n}\n\nfunc (d *HalalCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\treturn d.makeDir(ctx, parentDir, dirName)\n}\n\nfunc (d *HalalCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.move(ctx, srcObj, dstDir)\n}\n\nfunc (d *HalalCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\treturn d.rename(ctx, srcObj, newName)\n}\n\nfunc (d *HalalCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.copy(ctx, srcObj, dstDir)\n}\n\nfunc (d *HalalCloud) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.remove(ctx, obj)\n}\n\nfunc (d *HalalCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn d.put(ctx, dstDir, stream, up)\n}\n\nfunc (d *HalalCloud) IsLogin() bool {\n\tif d.AuthService.tr == nil {\n\t\treturn false\n\t}\n\tserv, err := d.NewAuthService(d.Addition.RefreshToken)\n\tif err != nil {\n\t\treturn false\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\tresult, err := pbPublicUser.NewPubUserClient(serv.GetGrpcConnection()).Get(ctx, &pbPublicUser.User{\n\t\tIdentity: \"\",\n\t})\n\tif result == nil || err != nil {\n\t\treturn false\n\t}\n\td.UserInfo.Identity = result.Identity\n\td.UserInfo.CreateTs = result.CreateTs\n\td.UserInfo.Name = result.Name\n\td.UserInfo.UpdateTs = result.UpdateTs\n\treturn true\n}\n\ntype HalalCommon struct {\n\t*Common\n\t*AuthService     // 登录信息\n\t*UserInfo        // 用户信息\n\trefreshTokenFunc func(token string) error\n\tserv             *AuthService\n}\n\nfunc (d *HalalCloud) SetTokenResp(tr *TokenResp) {\n\td.Addition.RefreshToken = tr.RefreshToken\n}\n\nfunc (d *HalalCloud) getFiles(ctx context.Context, dir model.Obj) ([]model.Obj, error) {\n\n\tfiles := make([]model.Obj, 0)\n\tlimit := int64(100)\n\ttoken := \"\"\n\tclient := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())\n\n\topDir := d.GetCurrentDir(dir)\n\n\tfor {\n\t\tresult, err := client.List(ctx, &pubUserFile.FileListRequest{\n\t\t\tParent: &pubUserFile.File{Path: opDir},\n\t\t\tListInfo: &common.ScanListRequest{\n\t\t\t\tLimit: limit,\n\t\t\t\tToken: token,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := 0; len(result.Files) > i; i++ {\n\t\t\tfiles = append(files, (*Files)(result.Files[i]))\n\t\t}\n\n\t\tif result.ListInfo == nil || result.ListInfo.Token == \"\" {\n\t\t\tbreak\n\t\t}\n\t\ttoken = result.ListInfo.Token\n\n\t}\n\treturn files, nil\n}\n\nfunc (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\n\tclient := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())\n\tctx1, cancelFunc := context.WithCancel(context.Background())\n\tdefer cancelFunc()\n\n\tresult, err := client.ParseFileSlice(ctx1, (*pubUserFile.File)(file.(*Files)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileAddrs := []*pubUserFile.SliceDownloadInfo{}\n\tvar addressDuration int64\n\n\tnodesNumber := len(result.RawNodes)\n\tnodesIndex := nodesNumber - 1\n\tstartIndex, endIndex := 0, nodesIndex\n\tfor nodesIndex >= 0 {\n\t\tif nodesIndex >= 200 {\n\t\t\tendIndex = 200\n\t\t} else {\n\t\t\tendIndex = nodesNumber\n\t\t}\n\t\tfor ; endIndex <= nodesNumber; endIndex += 200 {\n\t\t\tif endIndex == 0 {\n\t\t\t\tendIndex = 1\n\t\t\t}\n\t\t\tsliceAddress, err := client.GetSliceDownloadAddress(ctx, &pubUserFile.SliceDownloadAddressRequest{\n\t\t\t\tIdentity: result.RawNodes[startIndex:endIndex],\n\t\t\t\tVersion:  1,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\taddressDuration = sliceAddress.ExpireAt\n\t\t\tfileAddrs = append(fileAddrs, sliceAddress.Addresses...)\n\t\t\tstartIndex = endIndex\n\t\t\tnodesIndex -= 200\n\t\t}\n\n\t}\n\n\tsize := result.FileSize\n\tchunks := getChunkSizes(result.Sizes)\n\tresultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tlength := httpRange.Length\n\t\tif httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size {\n\t\t\tlength = size - httpRange.Start\n\t\t}\n\t\too := &openObject{\n\t\t\tctx:     ctx,\n\t\t\td:       fileAddrs,\n\t\t\tchunk:   &[]byte{},\n\t\t\tchunks:  &chunks,\n\t\t\tskip:    httpRange.Start,\n\t\t\tsha:     result.Sha1,\n\t\t\tshaTemp: sha1.New(),\n\t\t}\n\n\t\treturn readers.NewLimitedReadCloser(oo, length), nil\n\t}\n\n\tvar duration time.Duration\n\tif addressDuration != 0 {\n\t\tduration = time.Until(time.UnixMilli(addressDuration))\n\t} else {\n\t\tduration = time.Until(time.Now().Add(time.Hour))\n\t}\n\n\treturn &model.Link{\n\t\tRangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),\n\t\tExpiration:  &duration,\n\t}, nil\n}\n\nfunc (d *HalalCloud) makeDir(ctx context.Context, dir model.Obj, name string) (model.Obj, error) {\n\tnewDir := userfile.NewFormattedPath(d.GetCurrentOpDir(dir, []string{name}, 0)).GetPath()\n\t_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Create(ctx, &pubUserFile.File{\n\t\tPath: newDir,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloud) move(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {\n\toldDir := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()\n\tnewDir := userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath()\n\t_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Move(ctx, &pubUserFile.BatchOperationRequest{\n\t\tSource: []*pubUserFile.File{\n\t\t\t{\n\t\t\t\tIdentity: obj.GetID(),\n\t\t\t\tPath:     oldDir,\n\t\t\t},\n\t\t},\n\t\tDest: &pubUserFile.File{\n\t\t\tIdentity: dir.GetID(),\n\t\t\tPath:     newDir,\n\t\t},\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloud) rename(ctx context.Context, obj model.Obj, name string) (model.Obj, error) {\n\tid := obj.GetID()\n\tnewPath := userfile.NewFormattedPath(d.GetCurrentOpDir(obj, []string{name}, 0)).GetPath()\n\n\t_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Rename(ctx, &pubUserFile.File{\n\t\tPath:     newPath,\n\t\tIdentity: id,\n\t\tName:     name,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloud) copy(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {\n\tid := obj.GetID()\n\tsourcePath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()\n\tif len(id) > 0 {\n\t\tsourcePath = \"\"\n\t}\n\tdest := &pubUserFile.File{\n\t\tIdentity: dir.GetID(),\n\t\tPath:     userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath(),\n\t}\n\t_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Copy(ctx, &pubUserFile.BatchOperationRequest{\n\t\tSource: []*pubUserFile.File{\n\t\t\t{\n\t\t\t\tPath:     sourcePath,\n\t\t\t\tIdentity: id,\n\t\t\t},\n\t\t},\n\t\tDest: dest,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloud) remove(ctx context.Context, obj model.Obj) error {\n\tid := obj.GetID()\n\tnewPath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()\n\t//if len(id) > 0 {\n\t//\tnewPath = \"\"\n\t//}\n\t_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Delete(ctx, &pubUserFile.BatchOperationRequest{\n\t\tSource: []*pubUserFile.File{\n\t\t\t{\n\t\t\t\tPath:     newPath,\n\t\t\t\tIdentity: id,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (d *HalalCloud) put(ctx context.Context, dstDir model.Obj, fileStream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\n\tnewDir := path.Join(dstDir.GetPath(), fileStream.GetName())\n\n\tresult, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).CreateUploadToken(ctx, &pubUserFile.File{\n\t\tPath: newDir,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu, _ := url.Parse(result.Endpoint)\n\tu.Host = \"s3.\" + u.Host\n\tresult.Endpoint = u.String()\n\ts, err := session.NewSession(&aws.Config{\n\t\tHTTPClient:       base.HttpClient,\n\t\tCredentials:      credentials.NewStaticCredentials(result.AccessKey, result.SecretKey, result.Token),\n\t\tRegion:           aws.String(result.Region),\n\t\tEndpoint:         aws.String(result.Endpoint),\n\t\tS3ForcePathStyle: aws.Bool(true),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuploader := s3manager.NewUploader(s, func(u *s3manager.Uploader) {\n\t\tu.Concurrency = d.uploadThread\n\t})\n\tif fileStream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\tuploader.PartSize = fileStream.GetSize() / (s3manager.MaxUploadParts - 1)\n\t}\n\treader := driver.NewLimitedUploadStream(ctx, fileStream)\n\t_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{\n\t\tBucket: aws.String(result.Bucket),\n\t\tKey:    aws.String(result.Key),\n\t\tBody:   io.TeeReader(reader, driver.NewProgress(fileStream.GetSize(), up)),\n\t})\n\treturn nil, err\n\n}\n\nvar _ driver.Driver = (*HalalCloud)(nil)\n"
  },
  {
    "path": "drivers/halalcloud/meta.go",
    "content": "package halalcloud\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// define other\n\tRefreshToken string `json:\"refresh_token\" required:\"true\" help:\"login type is refresh_token,this is required\"`\n\tUploadThread string `json:\"upload_thread\" default:\"3\" help:\"1 <= thread <= 32\"`\n\n\tAppID      string `json:\"app_id\" required:\"true\" default:\"openlist/10001\"`\n\tAppVersion string `json:\"app_version\" required:\"true\" default:\"1.0.0\"`\n\tAppSecret  string `json:\"app_secret\" required:\"true\" default:\"bR4SJwOkvnG5WvVJ\"`\n}\n\nvar config = driver.Config{\n\tName:        \"HalalCloud\",\n\tOnlyProxy:   true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &HalalCloud{}\n\t})\n}\n"
  },
  {
    "path": "drivers/halalcloud/options.go",
    "content": "package halalcloud\n\nimport \"google.golang.org/grpc\"\n\nfunc defaultOptions() halalOptions {\n\treturn halalOptions{\n\t\t// onRefreshTokenRefreshed: func(string) {},\n\t\tgrpcOptions: []grpc.DialOption{\n\t\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 32)),\n\t\t\t// grpc.WithMaxMsgSize(1024 * 1024 * 1024),\n\t\t},\n\t}\n}\n\ntype HalalOption interface {\n\tapply(*halalOptions)\n}\n\n// halalOptions configure a RPC call. halalOptions are set by the HalalOption\n// values passed to Dial.\ntype halalOptions struct {\n\tonTokenRefreshed func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)\n\tgrpcOptions      []grpc.DialOption\n}\n\n// funcDialOption wraps a function that modifies halalOptions into an\n// implementation of the DialOption interface.\ntype funcDialOption struct {\n\tf func(*halalOptions)\n}\n\nfunc (fdo *funcDialOption) apply(do *halalOptions) {\n\tfdo.f(do)\n}\n\nfunc newFuncDialOption(f func(*halalOptions)) *funcDialOption {\n\treturn &funcDialOption{\n\t\tf: f,\n\t}\n}\n\nfunc WithRefreshTokenRefreshedCallback(s func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)) HalalOption {\n\treturn newFuncDialOption(func(o *halalOptions) {\n\t\to.onTokenRefreshed = s\n\t})\n}\n\nfunc WithGrpcDialOptions(opts ...grpc.DialOption) HalalOption {\n\treturn newFuncDialOption(func(o *halalOptions) {\n\t\to.grpcOptions = opts\n\t})\n}\n"
  },
  {
    "path": "drivers/halalcloud/types.go",
    "content": "package halalcloud\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/city404/v6-public-rpc-proto/go/v6/common\"\n\tpubUserFile \"github.com/city404/v6-public-rpc-proto/go/v6/userfile\"\n\t\"google.golang.org/grpc\"\n)\n\ntype AuthService struct {\n\tappID          string\n\tappVersion     string\n\tappSecret      string\n\tgrpcConnection *grpc.ClientConn\n\tdopts          halalOptions\n\ttr             *TokenResp\n}\n\ntype TokenResp struct {\n\tAccessToken           string `json:\"accessToken,omitempty\"`\n\tAccessTokenExpiredAt  int64  `json:\"accessTokenExpiredAt,omitempty\"`\n\tRefreshToken          string `json:\"refreshToken,omitempty\"`\n\tRefreshTokenExpiredAt int64  `json:\"refreshTokenExpiredAt,omitempty\"`\n}\n\ntype UserInfo struct {\n\tIdentity string `json:\"identity,omitempty\"`\n\tUpdateTs int64  `json:\"updateTs,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tCreateTs int64  `json:\"createTs,omitempty\"`\n}\n\ntype OrderByInfo struct {\n\tField string `json:\"field,omitempty\"`\n\tAsc   bool   `json:\"asc,omitempty\"`\n}\n\ntype ListInfo struct {\n\tToken   string         `json:\"token,omitempty\"`\n\tLimit   int64          `json:\"limit,omitempty\"`\n\tOrderBy []*OrderByInfo `json:\"order_by,omitempty\"`\n\tVersion int32          `json:\"version,omitempty\"`\n}\n\ntype FilesList struct {\n\tFiles    []*Files                `json:\"files,omitempty\"`\n\tListInfo *common.ScanListRequest `json:\"list_info,omitempty\"`\n}\n\nvar _ model.Obj = (*Files)(nil)\n\ntype Files pubUserFile.File\n\nfunc (f *Files) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f *Files) GetName() string {\n\treturn f.Name\n}\n\nfunc (f *Files) ModTime() time.Time {\n\treturn time.UnixMilli(f.UpdateTs)\n}\n\nfunc (f *Files) CreateTime() time.Time {\n\treturn time.UnixMilli(f.UpdateTs)\n}\n\nfunc (f *Files) IsDir() bool {\n\treturn f.Dir\n}\n\nfunc (f *Files) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *Files) GetID() string {\n\tif len(f.Identity) == 0 {\n\t\tf.Identity = \"/\"\n\t}\n\treturn f.Identity\n}\n\nfunc (f *Files) GetPath() string {\n\treturn f.Path\n}\n\ntype SteamFile struct {\n\tfile model.File\n}\n\nfunc (s *SteamFile) Read(p []byte) (n int, err error) {\n\treturn s.file.Read(p)\n}\n"
  },
  {
    "path": "drivers/halalcloud/util.go",
    "content": "package halalcloud\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tpbPublicUser \"github.com/city404/v6-public-rpc-proto/go/v6/user\"\n\tpubUserFile \"github.com/city404/v6-public-rpc-proto/go/v6/userfile\"\n\t\"github.com/google/uuid\"\n\t\"github.com/ipfs/go-cid\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n)\n\nconst (\n\tAppID      = \"alist/10001\"\n\tAppVersion = \"1.0.0\"\n\tAppSecret  = \"bR4SJwOkvnG5WvVJ\"\n)\n\nconst (\n\tgrpcServer     = \"grpcuserapi.2dland.cn:443\"\n\tgrpcServerAuth = \"grpcuserapi.2dland.cn\"\n)\n\nfunc (d *HalalCloud) NewAuthServiceWithOauth(options ...HalalOption) (*AuthService, error) {\n\n\taService := &AuthService{}\n\terr2 := errors.New(\"\")\n\n\tsvc := d.HalalCommon.AuthService\n\tfor _, opt := range options {\n\t\topt.apply(&svc.dopts)\n\t}\n\n\tgrpcOptions := svc.dopts.grpcOptions\n\tgrpcOptions = append(grpcOptions, grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {\n\t\tctxx := svc.signContext(method, ctx)\n\t\terr := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method\n\t\treturn err\n\t}))\n\n\tgrpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer grpcConnection.Close()\n\tuserClient := pbPublicUser.NewPubUserClient(grpcConnection)\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\tstateString := uuid.New().String()\n\t// queryValues.Add(\"callback\", oauthToken.Callback)\n\toauthToken, err := userClient.CreateAuthToken(ctx, &pbPublicUser.LoginRequest{\n\t\tReturnType: 2,\n\t\tState:      stateString,\n\t\tReturnUrl:  \"\",\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(oauthToken.State) < 1 {\n\t\toauthToken.State = stateString\n\t}\n\n\tif oauthToken.Url != \"\" {\n\n\t\treturn nil, fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, oauthToken.Url)\n\t}\n\n\treturn aService, err2\n\n}\n\nfunc (d *HalalCloud) NewAuthService(refreshToken string, options ...HalalOption) (*AuthService, error) {\n\tsvc := d.HalalCommon.AuthService\n\n\tif len(refreshToken) < 1 {\n\t\trefreshToken = d.Addition.RefreshToken\n\t}\n\n\tif len(d.tr.AccessToken) > 0 {\n\t\taccessTokenExpiredAt := d.tr.AccessTokenExpiredAt\n\t\tcurrent := time.Now().UnixMilli()\n\t\tif accessTokenExpiredAt < current {\n\t\t\t// access token expired\n\t\t\td.tr.AccessToken = \"\"\n\t\t\td.tr.AccessTokenExpiredAt = 0\n\t\t} else {\n\t\t\tsvc.tr.AccessTokenExpiredAt = accessTokenExpiredAt\n\t\t\tsvc.tr.AccessToken = d.tr.AccessToken\n\t\t}\n\t}\n\n\tfor _, opt := range options {\n\t\topt.apply(&svc.dopts)\n\t}\n\n\tgrpcOptions := svc.dopts.grpcOptions\n\tgrpcOptions = append(grpcOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(10*1024*1024), grpc.MaxCallRecvMsgSize(10*1024*1024)), grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {\n\t\tctxx := svc.signContext(method, ctx)\n\t\terr := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method\n\t\tif err != nil {\n\t\t\tgrpcStatus, ok := status.FromError(err)\n\n\t\t\tif ok && grpcStatus.Code() == codes.Unauthenticated && strings.Contains(grpcStatus.Err().Error(), \"invalid accesstoken\") && len(refreshToken) > 0 {\n\t\t\t\t// refresh token\n\t\t\t\trefreshResponse, err := pbPublicUser.NewPubUserClient(cc).Refresh(ctx, &pbPublicUser.Token{\n\t\t\t\t\tRefreshToken: refreshToken,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif len(refreshResponse.AccessToken) > 0 {\n\t\t\t\t\tsvc.tr.AccessToken = refreshResponse.AccessToken\n\t\t\t\t\tsvc.tr.AccessTokenExpiredAt = refreshResponse.AccessTokenExpireTs\n\t\t\t\t\tsvc.OnAccessTokenRefreshed(refreshResponse.AccessToken, refreshResponse.AccessTokenExpireTs, refreshResponse.RefreshToken, refreshResponse.RefreshTokenExpireTs)\n\t\t\t\t}\n\t\t\t\t// retry\n\t\t\t\tctxx := svc.signContext(method, ctx)\n\t\t\t\terr = invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}))\n\tgrpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsvc.grpcConnection = grpcConnection\n\treturn svc, err\n}\n\nfunc (s *AuthService) OnAccessTokenRefreshed(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64) {\n\ts.tr.AccessToken = accessToken\n\ts.tr.AccessTokenExpiredAt = accessTokenExpiredAt\n\ts.tr.RefreshToken = refreshToken\n\ts.tr.RefreshTokenExpiredAt = refreshTokenExpiredAt\n\n\tif s.dopts.onTokenRefreshed != nil {\n\t\ts.dopts.onTokenRefreshed(accessToken, accessTokenExpiredAt, refreshToken, refreshTokenExpiredAt)\n\t}\n\n}\n\nfunc (s *AuthService) GetGrpcConnection() *grpc.ClientConn {\n\treturn s.grpcConnection\n}\n\nfunc (s *AuthService) Close() {\n\t_ = s.grpcConnection.Close()\n}\n\nfunc (s *AuthService) signContext(method string, ctx context.Context) context.Context {\n\tvar kvString []string\n\tcurrentTimeStamp := strconv.FormatInt(time.Now().UnixMilli(), 10)\n\tbufferedString := bytes.NewBufferString(method)\n\tkvString = append(kvString, \"timestamp\", currentTimeStamp)\n\tbufferedString.WriteString(currentTimeStamp)\n\tkvString = append(kvString, \"appid\", s.appID)\n\tbufferedString.WriteString(s.appID)\n\tkvString = append(kvString, \"appversion\", s.appVersion)\n\tbufferedString.WriteString(s.appVersion)\n\tif s.tr != nil && len(s.tr.AccessToken) > 0 {\n\t\tauthorization := \"Bearer \" + s.tr.AccessToken\n\t\tkvString = append(kvString, \"authorization\", authorization)\n\t\tbufferedString.WriteString(authorization)\n\t}\n\tbufferedString.WriteString(s.appSecret)\n\tsign := GetMD5Hash(bufferedString.String())\n\tkvString = append(kvString, \"sign\", sign)\n\treturn metadata.AppendToOutgoingContext(ctx, kvString...)\n}\n\nfunc (d *HalalCloud) GetCurrentOpDir(dir model.Obj, args []string, index int) string {\n\tcurrentDir := dir.GetPath()\n\tif len(currentDir) == 0 {\n\t\tcurrentDir = \"/\"\n\t}\n\topPath := currentDir + \"/\" + args[index]\n\tif strings.HasPrefix(args[index], \"/\") {\n\t\topPath = args[index]\n\t}\n\treturn opPath\n}\n\nfunc (d *HalalCloud) GetCurrentDir(dir model.Obj) string {\n\tcurrentDir := dir.GetPath()\n\tif len(currentDir) == 0 {\n\t\tcurrentDir = \"/\"\n\t}\n\treturn currentDir\n}\n\ntype Common struct {\n}\n\nfunc getRawFiles(addr *pubUserFile.SliceDownloadInfo) ([]byte, error) {\n\n\tif addr == nil {\n\t\treturn nil, errors.New(\"addr is nil\")\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Duration(60 * time.Second), // Set timeout to 5 seconds\n\t}\n\tresp, err := client.Get(addr.DownloadAddress)\n\tif err != nil {\n\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"bad status: %s, body: %s\", resp.Status, body)\n\t}\n\n\tif addr.Encrypt > 0 {\n\t\tcd := uint8(addr.Encrypt)\n\t\tfor idx := 0; idx < len(body); idx++ {\n\t\t\tbody[idx] = body[idx] ^ cd\n\t\t}\n\t}\n\n\tif addr.StoreType != 10 {\n\n\t\tsourceCid, err := cid.Decode(addr.Identity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcheckCid, err := sourceCid.Prefix().Sum(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !checkCid.Equals(sourceCid) {\n\t\t\treturn nil, fmt.Errorf(\"bad cid: %s, body: %s\", checkCid.String(), body)\n\t\t}\n\t}\n\n\treturn body, nil\n\n}\n\ntype openObject struct {\n\tctx     context.Context\n\tmu      sync.Mutex\n\td       []*pubUserFile.SliceDownloadInfo\n\tid      int\n\tskip    int64\n\tchunk   *[]byte\n\tchunks  *[]chunkSize\n\tclosed  bool\n\tsha     string\n\tshaTemp hash.Hash\n}\n\n// get the next chunk\nfunc (oo *openObject) getChunk(ctx context.Context) (err error) {\n\tif oo.id >= len(*oo.chunks) {\n\t\treturn io.EOF\n\t}\n\tvar chunk []byte\n\terr = utils.Retry(3, time.Second, func() (err error) {\n\t\tchunk, err = getRawFiles(oo.d[oo.id])\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\too.id++\n\too.chunk = &chunk\n\treturn nil\n}\n\n// Read reads up to len(p) bytes into p.\nfunc (oo *openObject) Read(p []byte) (n int, err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn 0, fmt.Errorf(\"read on closed file\")\n\t}\n\t// Skip data at the start if requested\n\tfor oo.skip > 0 {\n\t\t//size := 1024 * 1024\n\t\t_, size, err := oo.ChunkLocation(oo.id)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip < int64(size) {\n\t\t\tbreak\n\t\t}\n\t\too.id++\n\t\too.skip -= int64(size)\n\t}\n\tif len(*oo.chunk) == 0 {\n\t\terr = oo.getChunk(oo.ctx)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip > 0 {\n\t\t\t*oo.chunk = (*oo.chunk)[oo.skip:]\n\t\t\too.skip = 0\n\t\t}\n\t}\n\tn = copy(p, *oo.chunk)\n\t*oo.chunk = (*oo.chunk)[n:]\n\n\too.shaTemp.Write(*oo.chunk)\n\n\treturn n, nil\n}\n\n// Close closed the file - MAC errors are reported here\nfunc (oo *openObject) Close() (err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn nil\n\t}\n\t// 校验Sha1\n\tif string(oo.shaTemp.Sum(nil)) != oo.sha {\n\t\treturn fmt.Errorf(\"failed to finish download: %w\", err)\n\t}\n\n\too.closed = true\n\treturn nil\n}\n\nfunc GetMD5Hash(text string) string {\n\ttHash := md5.Sum([]byte(text))\n\treturn hex.EncodeToString(tHash[:])\n}\n\n// chunkSize describes a size and position of chunk\ntype chunkSize struct {\n\tposition int64\n\tsize     int\n}\n\nfunc getChunkSizes(sliceSize []*pubUserFile.SliceSize) (chunks []chunkSize) {\n\tchunks = make([]chunkSize, 0)\n\tfor _, s := range sliceSize {\n\t\t// 对最后一个做特殊处理\n\t\tif s.EndIndex == 0 {\n\t\t\ts.EndIndex = s.StartIndex\n\t\t}\n\t\tfor j := s.StartIndex; j <= s.EndIndex; j++ {\n\t\t\tchunks = append(chunks, chunkSize{position: j, size: int(s.Size)})\n\t\t}\n\t}\n\treturn chunks\n}\n\nfunc (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) {\n\tif id < 0 || id >= len(*oo.chunks) {\n\t\treturn 0, 0, errors.New(\"invalid arguments\")\n\t}\n\n\treturn (*oo.chunks)[id].position, (*oo.chunks)[id].size, nil\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/common.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\tsdkUser \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/user\"\n)\n\nvar (\n\tslicePostErrorRetryInterval = time.Second * 120\n\tretryTimes                  = 5\n)\n\ntype halalCommon struct {\n\t// *AuthService     // 登录信息\n\tUserInfo         *sdkUser.User // 用户信息\n\trefreshTokenFunc func(token string) error\n\t// serv             *AuthService\n\tconfigs sync.Map\n}\n\nfunc (m *halalCommon) GetAccessToken() (string, error) {\n\tvalue, exists := m.configs.Load(\"access_token\")\n\tif !exists {\n\t\treturn \"\", nil // 如果不存在，返回空字符串\n\t}\n\treturn value.(string), nil // 返回配置项的值\n}\n\n// GetRefreshToken implements ConfigStore.\nfunc (m *halalCommon) GetRefreshToken() (string, error) {\n\tvalue, exists := m.configs.Load(\"refresh_token\")\n\tif !exists {\n\t\treturn \"\", nil // 如果不存在，返回空字符串\n\t}\n\treturn value.(string), nil // 返回配置项的值\n}\n\n// SetAccessToken implements ConfigStore.\nfunc (m *halalCommon) SetAccessToken(token string) error {\n\tm.configs.Store(\"access_token\", token)\n\treturn nil\n}\n\n// SetRefreshToken implements ConfigStore.\nfunc (m *halalCommon) SetRefreshToken(token string) error {\n\tm.configs.Store(\"refresh_token\", token)\n\tif m.refreshTokenFunc != nil {\n\t\treturn m.refreshTokenFunc(token)\n\t}\n\treturn nil\n}\n\n// SetToken implements ConfigStore.\nfunc (m *halalCommon) SetToken(accessToken string, refreshToken string, expiresIn int64) error {\n\tm.configs.Store(\"access_token\", accessToken)\n\tm.configs.Store(\"refresh_token\", refreshToken)\n\tm.configs.Store(\"expires_in\", expiresIn)\n\tif m.refreshTokenFunc != nil {\n\t\treturn m.refreshTokenFunc(refreshToken)\n\t}\n\treturn nil\n}\n\n// ClearConfigs implements ConfigStore.\nfunc (m *halalCommon) ClearConfigs() error {\n\tm.configs = sync.Map{} // 清空map\n\treturn nil\n}\n\n// DeleteConfig implements ConfigStore.\nfunc (m *halalCommon) DeleteConfig(key string) error {\n\t_, exists := m.configs.Load(key)\n\tif !exists {\n\t\treturn nil // 如果不存在，直接返回\n\t}\n\tm.configs.Delete(key) // 删除指定的配置项\n\treturn nil\n}\n\n// GetConfig implements ConfigStore.\nfunc (m *halalCommon) GetConfig(key string) (string, error) {\n\tvalue, exists := m.configs.Load(key)\n\tif !exists {\n\t\treturn \"\", nil // 如果不存在，返回空字符串\n\t}\n\treturn value.(string), nil // 返回配置项的值\n}\n\n// ListConfigs implements ConfigStore.\nfunc (m *halalCommon) ListConfigs() (map[string]string, error) {\n\tconfigs := make(map[string]string)\n\tm.configs.Range(func(key, value interface{}) bool {\n\t\tconfigs[key.(string)] = value.(string) // 将每个配置项添加到map中\n\t\treturn true                            // 继续遍历\n\t})\n\treturn configs, nil // 返回所有配置项\n}\n\n// SetConfig implements ConfigStore.\nfunc (m *halalCommon) SetConfig(key string, value string) error {\n\tm.configs.Store(key, value) // 使用Store方法设置或更新配置项\n\treturn nil                  // 成功设置配置项后返回nil\n}\n\nfunc NewHalalCommon() *halalCommon {\n\treturn &halalCommon{\n\t\tconfigs: sync.Map{},\n\t}\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/driver.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tsdkClient \"github.com/halalcloud/golang-sdk-lite/halalcloud/apiclient\"\n\tsdkUser \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/user\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n)\n\ntype HalalCloudOpen struct {\n\t*halalCommon\n\tmodel.Storage\n\tAddition\n\tsdkClient          *sdkClient.Client\n\tsdkUserFileService *sdkUserFile.UserFileService\n\tsdkUserService     *sdkUser.UserService\n\tuploadThread       int\n}\n\nfunc (d *HalalCloudOpen) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *HalalCloudOpen) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nvar _ driver.Driver = (*HalalCloudOpen)(nil)\n"
  },
  {
    "path": "drivers/halalcloud_open/driver_curd_impl.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tsdkModel \"github.com/halalcloud/golang-sdk-lite/halalcloud/model\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n)\n\nfunc (d *HalalCloudOpen) getFiles(ctx context.Context, dir model.Obj) ([]model.Obj, error) {\n\n\tfiles := make([]model.Obj, 0)\n\tlimit := int64(100)\n\ttoken := \"\"\n\n\tfor {\n\t\tresult, err := d.sdkUserFileService.List(ctx, &sdkUserFile.FileListRequest{\n\t\t\tParent: &sdkUserFile.File{Path: dir.GetPath()},\n\t\t\tListInfo: &sdkModel.ScanListRequest{\n\t\t\t\tLimit: limit,\n\t\t\t\tToken: token,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := 0; len(result.Files) > i; i++ {\n\t\t\tfiles = append(files, NewObjFile(result.Files[i]))\n\t\t}\n\n\t\tif result.ListInfo == nil || result.ListInfo.Token == \"\" {\n\t\t\tbreak\n\t\t}\n\t\ttoken = result.ListInfo.Token\n\n\t}\n\treturn files, nil\n}\n\nfunc (d *HalalCloudOpen) makeDir(ctx context.Context, dir model.Obj, name string) (model.Obj, error) {\n\t_, err := d.sdkUserFileService.Create(ctx, &sdkUserFile.File{\n\t\tPath: dir.GetPath(),\n\t\tName: name,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloudOpen) move(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {\n\toldDir := obj.GetPath()\n\tnewDir := dir.GetPath()\n\t_, err := d.sdkUserFileService.Move(ctx, &sdkUserFile.BatchOperationRequest{\n\t\tSource: []*sdkUserFile.File{\n\t\t\t{\n\t\t\t\tPath: oldDir,\n\t\t\t},\n\t\t},\n\t\tDest: &sdkUserFile.File{\n\t\t\tPath: newDir,\n\t\t},\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloudOpen) rename(ctx context.Context, obj model.Obj, name string) (model.Obj, error) {\n\n\t_, err := d.sdkUserFileService.Rename(ctx, &sdkUserFile.File{\n\t\tPath: obj.GetPath(),\n\t\tName: name,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloudOpen) copy(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {\n\tid := obj.GetID()\n\tsourcePath := obj.GetPath()\n\tif len(id) > 0 {\n\t\tsourcePath = \"\"\n\t}\n\n\tdestID := dir.GetID()\n\tdestPath := dir.GetPath()\n\tif len(destID) > 0 {\n\t\tdestPath = \"\"\n\t}\n\tdest := &sdkUserFile.File{\n\t\tPath:     destPath,\n\t\tIdentity: destID,\n\t}\n\t_, err := d.sdkUserFileService.Copy(ctx, &sdkUserFile.BatchOperationRequest{\n\t\tSource: []*sdkUserFile.File{\n\t\t\t{\n\t\t\t\tPath:     sourcePath,\n\t\t\t\tIdentity: id,\n\t\t\t},\n\t\t},\n\t\tDest: dest,\n\t})\n\treturn nil, err\n}\n\nfunc (d *HalalCloudOpen) remove(ctx context.Context, obj model.Obj) error {\n\tid := obj.GetID()\n\t_, err := d.sdkUserFileService.Delete(ctx, &sdkUserFile.BatchOperationRequest{\n\t\tSource: []*sdkUserFile.File{\n\t\t\t{\n\t\t\t\tIdentity: id,\n\t\t\t\tPath:     obj.GetPath(),\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (d *HalalCloudOpen) details(ctx context.Context) (*model.StorageDetails, error) {\n\tret, err := d.sdkUserService.GetStatisticsAndQuota(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: ret.DiskStatisticsQuota.BytesQuota,\n\t\t\tUsedSpace:  ret.DiskStatisticsQuota.BytesUsed,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/driver_get_link.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"io\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n\t\"github.com/rclone/rclone/lib/readers\"\n)\n\nfunc (d *HalalCloudOpen) getLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif args.Redirect {\n\t\t// return nil, model.ErrUnsupported\n\t\tfid := file.GetID()\n\t\tfpath := file.GetPath()\n\t\tif fid != \"\" {\n\t\t\tfpath = \"\"\n\t\t}\n\t\tfi, err := d.sdkUserFileService.GetDirectDownloadAddress(ctx, &sdkUserFile.DirectDownloadRequest{\n\t\t\tIdentity: fid,\n\t\t\tPath:     fpath,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\texpireAt := fi.ExpireAt\n\t\tduration := time.Until(time.UnixMilli(expireAt))\n\t\treturn &model.Link{\n\t\t\tURL:        fi.DownloadAddress,\n\t\t\tExpiration: &duration,\n\t\t}, nil\n\t}\n\tresult, err := d.sdkUserFileService.ParseFileSlice(ctx, &sdkUserFile.File{\n\t\tIdentity: file.GetID(),\n\t\tPath:     file.GetPath(),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileAddrs := []*sdkUserFile.SliceDownloadInfo{}\n\tvar addressDuration int64\n\n\tnodesNumber := len(result.RawNodes)\n\tnodesIndex := nodesNumber - 1\n\tstartIndex, endIndex := 0, nodesIndex\n\tfor nodesIndex >= 0 {\n\t\tif nodesIndex >= 200 {\n\t\t\tendIndex = 200\n\t\t} else {\n\t\t\tendIndex = nodesNumber\n\t\t}\n\t\tfor ; endIndex <= nodesNumber; endIndex += 200 {\n\t\t\tif endIndex == 0 {\n\t\t\t\tendIndex = 1\n\t\t\t}\n\t\t\tsliceAddress, err := d.sdkUserFileService.GetSliceDownloadAddress(ctx, &sdkUserFile.SliceDownloadAddressRequest{\n\t\t\t\tIdentity: result.RawNodes[startIndex:endIndex],\n\t\t\t\tVersion:  1,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\taddressDuration, _ = strconv.ParseInt(sliceAddress.ExpireAt, 10, 64)\n\t\t\tfileAddrs = append(fileAddrs, sliceAddress.Addresses...)\n\t\t\tstartIndex = endIndex\n\t\t\tnodesIndex -= 200\n\t\t}\n\n\t}\n\n\tsize, _ := strconv.ParseInt(result.FileSize, 10, 64)\n\tchunks := getChunkSizes(result.Sizes)\n\tresultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tlength := httpRange.Length\n\t\tif httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size {\n\t\t\tlength = size - httpRange.Start\n\t\t}\n\t\too := &openObject{\n\t\t\tctx:     ctx,\n\t\t\td:       fileAddrs,\n\t\t\tchunk:   []byte{},\n\t\t\tchunks:  chunks,\n\t\t\tskip:    httpRange.Start,\n\t\t\tsha:     result.Sha1,\n\t\t\tshaTemp: sha1.New(),\n\t\t}\n\n\t\treturn readers.NewLimitedReadCloser(oo, length), nil\n\t}\n\n\tvar duration time.Duration\n\tif addressDuration != 0 {\n\t\tduration = time.Until(time.UnixMilli(addressDuration))\n\t} else {\n\t\tduration = time.Until(time.Now().Add(time.Hour))\n\t}\n\n\treturn &model.Link{\n\t\tRangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),\n\t\tExpiration:  &duration,\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/driver_init.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/halalcloud/golang-sdk-lite/halalcloud/apiclient\"\n\tsdkUser \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/user\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n)\n\nfunc (d *HalalCloudOpen) Init(ctx context.Context) error {\n\tif d.uploadThread < 1 || d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 3, 3\n\t}\n\tif d.halalCommon == nil {\n\t\td.halalCommon = &halalCommon{\n\t\t\tUserInfo: &sdkUser.User{},\n\t\t\trefreshTokenFunc: func(token string) error {\n\t\t\t\td.Addition.RefreshToken = token\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\t}\n\tif d.Addition.RefreshToken != \"\" {\n\t\td.halalCommon.SetRefreshToken(d.Addition.RefreshToken)\n\t}\n\ttimeout := d.Addition.TimeOut\n\tif timeout <= 0 {\n\t\ttimeout = 60\n\t}\n\thost := d.Addition.Host\n\tif host == \"\" {\n\t\thost = \"openapi.2dland.cn\"\n\t}\n\n\tclient := apiclient.NewClient(nil, host, d.Addition.ClientID, d.Addition.ClientSecret, d.halalCommon, apiclient.WithTimeout(time.Second*time.Duration(timeout)))\n\td.sdkClient = client\n\td.sdkUserFileService = sdkUserFile.NewUserFileService(client)\n\td.sdkUserService = sdkUser.NewUserService(client)\n\tuserInfo, err := d.sdkUserService.Get(ctx, &sdkUser.User{})\n\tif err != nil {\n\t\treturn err\n\t}\n\td.halalCommon.UserInfo = userInfo\n\t// 能够获取到用户信息，已经检查了 RefreshToken 的有效性，无需再次检查\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/driver_interface.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nfunc (d *HalalCloudOpen) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *HalalCloudOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.getFiles(ctx, dir)\n}\n\nfunc (d *HalalCloudOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\treturn d.getLink(ctx, file, args)\n}\n\nfunc (d *HalalCloudOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\treturn d.makeDir(ctx, parentDir, dirName)\n}\n\nfunc (d *HalalCloudOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.move(ctx, srcObj, dstDir)\n}\n\nfunc (d *HalalCloudOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\treturn d.rename(ctx, srcObj, newName)\n}\n\nfunc (d *HalalCloudOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.copy(ctx, srcObj, dstDir)\n}\n\nfunc (d *HalalCloudOpen) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.remove(ctx, obj)\n}\n\nfunc (d *HalalCloudOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn d.put(ctx, dstDir, stream, up)\n}\n\nfunc (d *HalalCloudOpen) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\treturn d.details(ctx)\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/halalcloud_upload.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n\t\"github.com/ipfs/go-cid\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *HalalCloudOpen) put(ctx context.Context, dstDir model.Obj, fileStream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\n\tnewPath := path.Join(dstDir.GetPath(), fileStream.GetName())\n\n\tuploadTask, err := d.sdkUserFileService.CreateUploadTask(ctx, &sdkUserFile.File{\n\t\tPath: newPath,\n\t\tSize: fileStream.GetSize(),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif uploadTask.Created {\n\t\treturn nil, nil\n\t}\n\n\tslicesList := make([]string, 0)\n\tcodec := uint64(0x55)\n\tif uploadTask.BlockCodec > 0 {\n\t\tcodec = uint64(uploadTask.BlockCodec)\n\t}\n\tblockHashType := uploadTask.BlockHashType\n\tmhType := uint64(0x12)\n\tif blockHashType > 0 {\n\t\tmhType = uint64(blockHashType)\n\t}\n\tprefix := cid.Prefix{\n\t\tCodec:    codec,\n\t\tMhLength: -1,\n\t\tMhType:   mhType,\n\t\tVersion:  1,\n\t}\n\tblockSize := uploadTask.BlockSize\n\t//\n\t// Not sure whether FileStream supports concurrent read and write operations, so currently using single-threaded upload to ensure safety.\n\t// read file\n\tbufferSize := int(blockSize)\n\tbuffer := make([]byte, bufferSize)\n\toffset := 0\n\tteeReader := io.TeeReader(fileStream, driver.NewProgress(fileStream.GetSize(), up))\n\tfor {\n\t\tn, err := teeReader.Read(buffer[offset:]) // 这里 len(buf[offset:]) <= 4MB\n\t\tif n > 0 {\n\t\t\toffset += n\n\t\t\tif offset == int(blockSize) {\n\t\t\t\tuploadCid, err := postFileSlice(ctx, buffer, uploadTask.Task, uploadTask.UploadAddress, prefix, retryTimes)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tslicesList = append(slicesList, uploadCid.String())\n\t\t\t\toffset = 0\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tif offset > 0 {\n\t\t\t\t\tuploadCid, err := postFileSlice(ctx, buffer[:offset], uploadTask.Task, uploadTask.UploadAddress, prefix, retryTimes)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tslicesList = append(slicesList, uploadCid.String())\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tnewFile, err := makeFile(ctx, slicesList, uploadTask.Task, uploadTask.UploadAddress, retryTimes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewObjFile(newFile), nil\n\n}\n\nfunc makeFile(ctx context.Context, fileSlice []string, taskID string, uploadAddress string, retry int) (*sdkUserFile.File, error) {\n\tvar lastError error = nil\n\tfor range retry {\n\t\tnewFile, err := doMakeFile(fileSlice, taskID, uploadAddress)\n\t\tif err == nil {\n\t\t\treturn newFile, nil\n\t\t}\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Errorf(\"make file slice failed, retrying... error: %s\", err.Error())\n\t\tif strings.Contains(err.Error(), \"not found\") {\n\t\t\treturn nil, err\n\t\t}\n\t\tlastError = err\n\t\ttime.Sleep(slicePostErrorRetryInterval)\n\t}\n\treturn nil, fmt.Errorf(\"mk file slice failed after %d times, error: %s\", retry, lastError.Error())\n}\n\nfunc doMakeFile(fileSlice []string, taskID string, uploadAddress string) (*sdkUserFile.File, error) {\n\taccessUrl := uploadAddress + \"/\" + taskID\n\tgetTimeOut := time.Minute * 2\n\tu, err := url.Parse(accessUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tn, _ := json.Marshal(fileSlice)\n\thttpRequest := http.Request{\n\t\tMethod: http.MethodPost,\n\t\tURL:    u,\n\t\tHeader: map[string][]string{\n\t\t\t\"Accept\":       {\"application/json\"},\n\t\t\t\"Content-Type\": {\"application/json\"},\n\t\t\t//\"Content-Length\": {strconv.Itoa(len(n))},\n\t\t},\n\t\tBody: io.NopCloser(bytes.NewReader(n)),\n\t}\n\thttpClient := http.Client{\n\t\tTimeout: getTimeOut,\n\t}\n\thttpResponse, err := httpClient.Do(&httpRequest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer httpResponse.Body.Close()\n\tif httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated {\n\t\tb, _ := io.ReadAll(httpResponse.Body)\n\t\tmessage := string(b)\n\t\tlog.Errorf(\"make file failed, status code: %d, message: %s\", httpResponse.StatusCode, message)\n\n\t\treturn nil, fmt.Errorf(\"mk file slice failed, status code: %d, message: %s\", httpResponse.StatusCode, message)\n\t}\n\tb, _ := io.ReadAll(httpResponse.Body)\n\tvar result *UploadedFile\n\terr = json.Unmarshal(b, &result)\n\tif err != nil {\n\t\tlog.Errorf(\"make file failed from response, status code: %d, message: %s\", httpResponse.StatusCode, string(b))\n\t\treturn nil, err\n\t}\n\treturn &sdkUserFile.File{\n\t\tIdentity:        result.Identity,\n\t\tPath:            result.Path,\n\t\tSize:            result.Size,\n\t\tContentIdentity: result.ContentIdentity,\n\t}, nil\n}\nfunc postFileSlice(ctx context.Context, fileSlice []byte, taskID string, uploadAddress string, preix cid.Prefix, retry int) (cid.Cid, error) {\n\tvar lastError error = nil\n\tfor range retry {\n\t\tnewCid, err := doPostFileSlice(fileSlice, taskID, uploadAddress, preix)\n\t\tif err == nil {\n\t\t\treturn newCid, nil\n\t\t}\n\t\tif ctx.Err() != nil {\n\t\t\treturn cid.Undef, err\n\t\t}\n\t\ttime.Sleep(slicePostErrorRetryInterval)\n\t\tlastError = err\n\t}\n\treturn cid.Undef, fmt.Errorf(\"upload file slice failed after %d times, error: %s\", retry, lastError.Error())\n}\nfunc doPostFileSlice(fileSlice []byte, taskID string, uploadAddress string, preix cid.Prefix) (cid.Cid, error) {\n\t// 1. sum file slice\n\tnewCid, err := preix.Sum(fileSlice)\n\tif err != nil {\n\t\treturn cid.Undef, err\n\t}\n\t// 2. post file slice\n\tsliceCidString := newCid.String()\n\t// /{taskID}/{sliceID}\n\taccessUrl := uploadAddress + \"/\" + taskID + \"/\" + sliceCidString\n\tgetTimeOut := time.Second * 30\n\t// get {accessUrl} in {getTimeOut}\n\tu, err := url.Parse(accessUrl)\n\tif err != nil {\n\t\treturn cid.Undef, err\n\t}\n\t// header: accept: application/json\n\t// header: content-type: application/octet-stream\n\t// header: content-length: {fileSlice.length}\n\t// header: x-content-cid: {sliceCidString}\n\t// header: x-task-id: {taskID}\n\thttpRequest := http.Request{\n\t\tMethod: http.MethodGet,\n\t\tURL:    u,\n\t\tHeader: map[string][]string{\n\t\t\t\"Accept\": {\"application/json\"},\n\t\t},\n\t}\n\thttpClient := http.Client{\n\t\tTimeout: getTimeOut,\n\t}\n\thttpResponse, err := httpClient.Do(&httpRequest)\n\tif err != nil {\n\t\tlog.Errorf(\"access %s failed, method: %s\", accessUrl, http.MethodGet)\n\t\treturn cid.Undef, err\n\t}\n\tif httpResponse.StatusCode != http.StatusOK {\n\t\tlog.Errorf(\"access %s failed, method: %s, status code: %d\", accessUrl, http.MethodGet, httpResponse.StatusCode)\n\t\treturn cid.Undef, fmt.Errorf(\"upload file slice failed, status code: %d\", httpResponse.StatusCode)\n\t}\n\tvar result bool\n\tb, err := io.ReadAll(httpResponse.Body)\n\tif err != nil {\n\t\treturn cid.Undef, err\n\t}\n\terr = json.Unmarshal(b, &result)\n\tif err != nil {\n\t\treturn cid.Undef, err\n\t}\n\tif result {\n\t\treturn newCid, nil\n\t}\n\n\thttpRequest = http.Request{\n\t\tMethod: http.MethodPost,\n\t\tURL:    u,\n\t\tHeader: map[string][]string{\n\t\t\t\"Accept\":       {\"application/json\"},\n\t\t\t\"Content-Type\": {\"application/octet-stream\"},\n\t\t\t// \"Content-Length\": {strconv.Itoa(len(fileSlice))},\n\t\t},\n\t\tBody: io.NopCloser(bytes.NewReader(fileSlice)),\n\t}\n\thttpResponse, err = httpClient.Do(&httpRequest)\n\tif err != nil {\n\t\treturn cid.Undef, err\n\t}\n\tdefer httpResponse.Body.Close()\n\tif httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated {\n\t\tb, _ := io.ReadAll(httpResponse.Body)\n\t\tmessage := string(b)\n\t\tlog.Errorf(\"upload file slice failed, status code: %d, message: %s\", httpResponse.StatusCode, message)\n\t\treturn cid.Undef, fmt.Errorf(\"upload file slice failed, status code: %d, message: %s\", httpResponse.StatusCode, message)\n\t}\n\t//\n\n\treturn newCid, nil\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/meta.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// define other\n\tRefreshToken string `json:\"refresh_token\" required:\"false\" help:\"If using a personal API approach, the RefreshToken is not required.\"`\n\tUploadThread int    `json:\"upload_thread\" type:\"number\" default:\"3\" help:\"1 <= thread <= 32\"`\n\n\tClientID     string `json:\"client_id\" required:\"true\" default:\"\"`\n\tClientSecret string `json:\"client_secret\" required:\"true\" default:\"\"`\n\tHost         string `json:\"host\" required:\"false\" default:\"openapi.2dland.cn\"`\n\tTimeOut      int    `json:\"timeout\" type:\"number\" default:\"60\" help:\"timeout in seconds\"`\n}\n\nvar config = driver.Config{\n\tName:        \"HalalCloudOpen\",\n\tOnlyProxy:   false,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &HalalCloudOpen{}\n\t})\n}\n\ntype UploadedFile struct {\n\tIdentity        string `json:\"identity\"`\n\tUserIdentity    string `json:\"user_identity\"`\n\tPath            string `json:\"path\"`\n\tSize            int64  `json:\"size\"`\n\tContentIdentity string `json:\"content_identity\"`\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/obj_file.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n)\n\ntype ObjFile struct {\n\tsdkFile    *sdkUserFile.File\n\tfileSize   int64\n\tmodTime    time.Time\n\tcreateTime time.Time\n}\n\nfunc NewObjFile(f *sdkUserFile.File) model.Obj {\n\tofile := &ObjFile{sdkFile: f}\n\tofile.fileSize = f.Size\n\tmodTimeTs := f.UpdateTs\n\tofile.modTime = time.UnixMilli(modTimeTs)\n\tcreateTimeTs := f.CreateTs\n\tofile.createTime = time.UnixMilli(createTimeTs)\n\treturn ofile\n}\n\nfunc (f *ObjFile) GetSize() int64 {\n\treturn f.fileSize\n}\n\nfunc (f *ObjFile) GetName() string {\n\treturn f.sdkFile.Name\n}\n\nfunc (f *ObjFile) ModTime() time.Time {\n\treturn f.modTime\n}\n\nfunc (f *ObjFile) IsDir() bool {\n\treturn f.sdkFile.Dir\n}\n\nfunc (f *ObjFile) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{\n\t\t// TODO: support more hash types\n\t}\n}\n\nfunc (f *ObjFile) GetID() string {\n\treturn f.sdkFile.Identity\n}\n\nfunc (f *ObjFile) GetPath() string {\n\treturn f.sdkFile.Path\n}\n\nfunc (f *ObjFile) CreateTime() time.Time {\n\treturn f.createTime\n}\n"
  },
  {
    "path": "drivers/halalcloud_open/utils.go",
    "content": "package halalcloudopen\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tsdkUserFile \"github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile\"\n\t\"github.com/ipfs/go-cid\"\n)\n\n// get the next chunk\nfunc (oo *openObject) getChunk(_ context.Context) (err error) {\n\tif oo.id >= len(oo.chunks) {\n\t\treturn io.EOF\n\t}\n\tvar chunk []byte\n\terr = utils.Retry(3, time.Second, func() (err error) {\n\t\tchunk, err = getRawFiles(oo.d[oo.id])\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\too.id++\n\too.chunk = chunk\n\treturn nil\n}\n\n// Read reads up to len(p) bytes into p.\nfunc (oo *openObject) Read(p []byte) (n int, err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn 0, fmt.Errorf(\"read on closed file\")\n\t}\n\t// Skip data at the start if requested\n\tfor oo.skip > 0 {\n\t\t//size := 1024 * 1024\n\t\t_, size, err := oo.ChunkLocation(oo.id)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip < int64(size) {\n\t\t\tbreak\n\t\t}\n\t\too.id++\n\t\too.skip -= int64(size)\n\t}\n\tif len(oo.chunk) == 0 {\n\t\terr = oo.getChunk(oo.ctx)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip > 0 {\n\t\t\too.chunk = (oo.chunk)[oo.skip:]\n\t\t\too.skip = 0\n\t\t}\n\t}\n\tn = copy(p, oo.chunk)\n\too.shaTemp.Write(p[:n])\n\too.chunk = (oo.chunk)[n:]\n\treturn n, nil\n}\n\n// Close closed the file - MAC errors are reported here\nfunc (oo *openObject) Close() (err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn nil\n\t}\n\t// 校验Sha1\n\tif string(oo.shaTemp.Sum(nil)) != oo.sha {\n\t\treturn fmt.Errorf(\"failed to finish download: SHA mismatch\")\n\t}\n\n\too.closed = true\n\treturn nil\n}\n\nfunc GetMD5Hash(text string) string {\n\ttHash := md5.Sum([]byte(text))\n\treturn hex.EncodeToString(tHash[:])\n}\n\ntype chunkSize struct {\n\tposition int64\n\tsize     int\n}\n\ntype openObject struct {\n\tctx     context.Context\n\tmu      sync.Mutex\n\td       []*sdkUserFile.SliceDownloadInfo\n\tid      int\n\tskip    int64\n\tchunk   []byte\n\tchunks  []chunkSize\n\tclosed  bool\n\tsha     string\n\tshaTemp hash.Hash\n}\n\nfunc getChunkSizes(sliceSize []*sdkUserFile.SliceSize) (chunks []chunkSize) {\n\tchunks = make([]chunkSize, 0)\n\tfor _, s := range sliceSize {\n\t\t// 对最后一个做特殊处理\n\t\tendIndex := s.EndIndex\n\t\tstartIndex := s.StartIndex\n\t\tif endIndex == 0 {\n\t\t\tendIndex = startIndex\n\t\t}\n\t\tfor j := startIndex; j <= endIndex; j++ {\n\t\t\tsize := s.Size\n\t\t\tchunks = append(chunks, chunkSize{position: j, size: int(size)})\n\t\t}\n\t}\n\treturn chunks\n}\n\nfunc (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) {\n\tif id < 0 || id >= len(oo.chunks) {\n\t\treturn 0, 0, errors.New(\"invalid arguments\")\n\t}\n\n\treturn (oo.chunks)[id].position, (oo.chunks)[id].size, nil\n}\n\nfunc getRawFiles(addr *sdkUserFile.SliceDownloadInfo) ([]byte, error) {\n\n\tif addr == nil {\n\t\treturn nil, errors.New(\"addr is nil\")\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Duration(60 * time.Second), // Set timeout to 60 seconds\n\t}\n\tresp, err := client.Get(addr.DownloadAddress)\n\tif err != nil {\n\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"bad status: %s, body: %s\", resp.Status, body)\n\t}\n\n\tif addr.Encrypt > 0 {\n\t\tcd := uint8(addr.Encrypt)\n\t\tfor idx := 0; idx < len(body); idx++ {\n\t\t\tbody[idx] = body[idx] ^ cd\n\t\t}\n\t}\n\tstoreType := addr.StoreType\n\tif storeType != 10 {\n\n\t\tsourceCid, err := cid.Decode(addr.Identity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcheckCid, err := sourceCid.Prefix().Sum(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !checkCid.Equals(sourceCid) {\n\t\t\treturn nil, fmt.Errorf(\"bad cid: %s, body: %s\", checkCid.String(), body)\n\t\t}\n\t}\n\n\treturn body, nil\n\n}\n"
  },
  {
    "path": "drivers/ilanzou/driver.go",
    "content": "package template\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/foxxorcat/mopan-sdk-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype ILanZou struct {\n\tmodel.Storage\n\tAddition\n\n\tuserID   string\n\taccount  string\n\tupClient *resty.Client\n\tconf     Conf\n\tconfig   driver.Config\n}\n\nfunc (d *ILanZou) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *ILanZou) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *ILanZou) Init(ctx context.Context) error {\n\td.upClient = base.NewRestyClient().SetTimeout(time.Minute * 10)\n\tif d.UUID == \"\" {\n\t\tres, err := d.unproved(\"/getUuid\", http.MethodGet, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.UUID = utils.Json.Get(res, \"uuid\").ToString()\n\t}\n\tres, err := d.proved(\"/user/account/map\", http.MethodGet, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.userID = utils.Json.Get(res, \"map\", \"userId\").ToString()\n\td.account = utils.Json.Get(res, \"map\", \"account\").ToString()\n\tlog.Debugf(\"[ilanzou] init response: %s\", res)\n\treturn nil\n}\n\nfunc (d *ILanZou) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\toffset := 1\n\tvar res []ListItem\n\tfor {\n\t\tvar resp ListResp\n\t\t_, err := d.proved(\"/record/file/list\", http.MethodGet, func(req *resty.Request) {\n\t\t\tparams := []string{\n\t\t\t\t\"offset=\" + strconv.Itoa(offset),\n\t\t\t\t\"limit=60\",\n\t\t\t\t\"folderId=\" + dir.GetID(),\n\t\t\t\t\"type=0\",\n\t\t\t}\n\t\t\tqueryString := strings.Join(params, \"&\")\n\t\t\treq.SetQueryString(queryString).SetResult(&resp)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, resp.List...)\n\t\tif resp.Offset < resp.TotalPage {\n\t\t\toffset++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn utils.SliceConvert(res, func(f ListItem) (model.Obj, error) {\n\t\tupdTime, err := time.ParseInLocation(\"2006-01-02 15:04:05\", f.UpdTime, time.Local)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tobj := model.Object{\n\t\t\tID: strconv.FormatInt(f.FileId, 10),\n\t\t\t// Path:     \"\",\n\t\t\tName:     f.FileName,\n\t\t\tSize:     f.FileSize * 1024,\n\t\t\tModified: updTime,\n\t\t\tCtime:    updTime,\n\t\t\tIsFolder: false,\n\t\t\t// HashInfo: utils.HashInfo{},\n\t\t}\n\t\tif f.FileType == 2 {\n\t\t\tobj.IsFolder = true\n\t\t\tobj.Size = 0\n\t\t\tobj.ID = strconv.FormatInt(f.FolderId, 10)\n\t\t\tobj.Name = f.FolderName\n\t\t}\n\t\treturn &obj, nil\n\t})\n}\n\nfunc (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tu, err := url.Parse(d.conf.base + \"/\" + d.conf.unproved + \"/file/redirect\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tts, ts_str, _ := getTimestamp(d.conf.secret)\n\n\tparams := []string{\n\t\t\"uuid=\" + url.QueryEscape(d.UUID),\n\t\t\"devType=6\",\n\t\t\"devCode=\" + url.QueryEscape(d.UUID),\n\t\t\"devModel=chrome\",\n\t\t\"devVersion=\" + url.QueryEscape(d.conf.devVersion),\n\t\t\"appVersion=\",\n\t\t\"timestamp=\" + ts_str,\n\t\t\"appToken=\" + url.QueryEscape(d.Token),\n\t\t\"enable=0\",\n\t}\n\n\tdownloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf(\"%s|%s\", file.GetID(), d.userID)), d.conf.secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tparams = append(params, \"downloadId=\"+url.QueryEscape(hex.EncodeToString(downloadId)))\n\n\tauth, err := mopan.AesEncrypt([]byte(fmt.Sprintf(\"%s|%d\", file.GetID(), ts)), d.conf.secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tparams = append(params, \"auth=\"+url.QueryEscape(hex.EncodeToString(auth)))\n\n\tu.RawQuery = strings.Join(params, \"&\")\n\trealURL := u.String()\n\t// get the url after redirect\n\treq := base.NoRedirectClient.R()\n\n\treq.SetHeaders(map[string]string{\n\t\t\"Referer\": d.conf.site + \"/\",\n\t})\n\tif d.Addition.Ip != \"\" {\n\t\treq.SetHeader(\"X-Forwarded-For\", d.Addition.Ip)\n\t}\n\n\tres, err := req.Get(realURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() == 302 {\n\t\trealURL = res.Header().Get(\"location\")\n\t} else {\n\t\treturn nil, fmt.Errorf(\"redirect failed, status: %d, msg: %s\", res.StatusCode(), utils.Json.Get(res.Body(), \"msg\").ToString())\n\t}\n\tlink := model.Link{URL: realURL}\n\treturn &link, nil\n}\n\nfunc (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tres, err := d.proved(\"/file/folder/save\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"folderDesc\": \"\",\n\t\t\t\"folderId\":   parentDir.GetID(),\n\t\t\t\"folderName\": dirName,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{\n\t\tID: utils.Json.Get(res, \"list\", 0, \"id\").ToString(),\n\t\t// Path:     \"\",\n\t\tName:     dirName,\n\t\tSize:     0,\n\t\tModified: time.Now(),\n\t\tCtime:    time.Now(),\n\t\tIsFolder: true,\n\t\t// HashInfo: utils.HashInfo{},\n\t}, nil\n}\n\nfunc (d *ILanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar fileIds, folderIds []string\n\tif srcObj.IsDir() {\n\t\tfolderIds = []string{srcObj.GetID()}\n\t} else {\n\t\tfileIds = []string{srcObj.GetID()}\n\t}\n\t_, err := d.proved(\"/file/folder/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"folderIds\": strings.Join(folderIds, \",\"),\n\t\t\t\"fileIds\":   strings.Join(fileIds, \",\"),\n\t\t\t\"targetId\":  dstDir.GetID(),\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *ILanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tvar err error\n\tif srcObj.IsDir() {\n\t\t_, err = d.proved(\"/file/folder/edit\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"folderDesc\": \"\",\n\t\t\t\t\"folderId\":   srcObj.GetID(),\n\t\t\t\t\"folderName\": newName,\n\t\t\t})\n\t\t})\n\t} else {\n\t\t_, err = d.proved(\"/file/edit\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(base.Json{\n\t\t\t\t\"fileDesc\": \"\",\n\t\t\t\t\"fileId\":   srcObj.GetID(),\n\t\t\t\t\"fileName\": newName,\n\t\t\t})\n\t\t})\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{\n\t\tID: srcObj.GetID(),\n\t\t// Path:     \"\",\n\t\tName:     newName,\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: time.Now(),\n\t\tCtime:    srcObj.CreateTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *ILanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO copy obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *ILanZou) Remove(ctx context.Context, obj model.Obj) error {\n\tvar fileIds, folderIds []string\n\tif obj.IsDir() {\n\t\tfolderIds = []string{obj.GetID()}\n\t} else {\n\t\tfileIds = []string{obj.GetID()}\n\t}\n\t_, err := d.proved(\"/file/delete\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"folderIds\": strings.Join(folderIds, \",\"),\n\t\t\t\"fileIds\":   strings.Join(fileIds, \",\"),\n\t\t\t\"status\":    0,\n\t\t})\n\t})\n\treturn err\n}\n\nconst DefaultPartSize = 1024 * 1024 * 8\n\nfunc (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tetag := s.GetHash().GetHash(utils.MD5)\n\tvar err error\n\tif len(etag) != utils.MD5.Width {\n\t\t_, etag, err = stream.CacheFullAndHash(s, &up, utils.MD5)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// get upToken\n\tres, err := d.proved(\"/7n/getUpToken\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileId\":   \"\",\n\t\t\t\"fileName\": s.GetName(),\n\t\t\t\"fileSize\": s.GetSize()/1024 + 1,\n\t\t\t\"folderId\": dstDir.GetID(),\n\t\t\t\"md5\":      etag,\n\t\t\t\"type\":     1,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tupToken := utils.Json.Get(res, \"upToken\").ToString()\n\tif upToken == \"-1\" {\n\t\t// 支持秒传\n\t\tvar resp UploadTokenRapidResp\n\t\terr := utils.Json.Unmarshal(res, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &model.Object{\n\t\t\tID:       strconv.FormatInt(resp.Map.FileID, 10),\n\t\t\tName:     resp.Map.FileName,\n\t\t\tSize:     s.GetSize(),\n\t\t\tModified: s.ModTime(),\n\t\t\tCtime:    s.CreateTime(),\n\t\t\tIsFolder: false,\n\t\t\tHashInfo: utils.NewHashInfo(utils.MD5, etag),\n\t\t}, nil\n\t}\n\tnow := time.Now()\n\tkey := fmt.Sprintf(\"disk/%d/%d/%d/%s/%016d\", now.Year(), now.Month(), now.Day(), d.account, now.UnixMilli())\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\tReader: s,\n\t\t\tSize:   s.GetSize(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\tvar token string\n\tif s.GetSize() <= DefaultPartSize {\n\t\tres, err := d.upClient.R().SetContext(ctx).SetMultipartFormData(map[string]string{\n\t\t\t\"token\": upToken,\n\t\t\t\"key\":   key,\n\t\t\t\"fname\": s.GetName(),\n\t\t}).SetMultipartField(\"file\", s.GetName(), s.GetMimetype(), reader).\n\t\t\tPost(\"https://upload.qiniup.com/\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttoken = utils.Json.Get(res.Body(), \"token\").ToString()\n\t} else {\n\t\tkeyBase64 := base64.URLEncoding.EncodeToString([]byte(key))\n\t\tres, err := d.upClient.R().SetHeader(\"Authorization\", \"UpToken \"+upToken).Post(fmt.Sprintf(\"https://upload.qiniup.com/buckets/%s/objects/%s/uploads\", d.conf.bucket, keyBase64))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuploadId := utils.Json.Get(res.Body(), \"uploadId\").ToString()\n\t\tparts := make([]Part, 0)\n\t\tpartNum := (s.GetSize() + DefaultPartSize - 1) / DefaultPartSize\n\t\tfor i := 1; i <= int(partNum); i++ {\n\t\t\tu := fmt.Sprintf(\"https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s/%d\", d.conf.bucket, keyBase64, uploadId, i)\n\t\t\tres, err = d.upClient.R().SetContext(ctx).SetHeader(\"Authorization\", \"UpToken \"+upToken).SetBody(io.LimitReader(reader, DefaultPartSize)).Put(u)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tetag := utils.Json.Get(res.Body(), \"etag\").ToString()\n\t\t\tparts = append(parts, Part{\n\t\t\t\tPartNumber: i,\n\t\t\t\tETag:       etag,\n\t\t\t})\n\t\t}\n\t\tres, err = d.upClient.R().SetHeader(\"Authorization\", \"UpToken \"+upToken).SetBody(base.Json{\n\t\t\t\"fnmae\": s.GetName(),\n\t\t\t\"parts\": parts,\n\t\t}).Post(fmt.Sprintf(\"https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s\", d.conf.bucket, keyBase64, uploadId))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttoken = utils.Json.Get(res.Body(), \"token\").ToString()\n\t}\n\t// commit upload\n\tvar resp UploadResultResp\n\tfor i := 0; i < 10; i++ {\n\t\t_, err = d.unproved(\"/7n/results\", http.MethodPost, func(req *resty.Request) {\n\t\t\tparams := []string{\n\t\t\t\t\"tokenList=\" + token,\n\t\t\t\t\"tokenTime=\" + time.Now().Format(\"Mon Jan 02 2006 15:04:05 GMT-0700 (MST)\"),\n\t\t\t}\n\t\t\tqueryString := strings.Join(params, \"&\")\n\t\t\treq.SetQueryString(queryString).SetResult(&resp)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(resp.List) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"upload failed, empty response\")\n\t\t}\n\t\tif resp.List[0].Status == 1 {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(time.Second * 1)\n\t}\n\tfile := resp.List[0]\n\tif file.Status != 1 {\n\t\treturn nil, fmt.Errorf(\"upload failed, status: %d\", resp.List[0].Status)\n\t}\n\treturn &model.Object{\n\t\tID: strconv.FormatInt(file.FileId, 10),\n\t\t// Path:     ,\n\t\tName:     file.FileName,\n\t\tSize:     s.GetSize(),\n\t\tModified: s.ModTime(),\n\t\tCtime:    s.CreateTime(),\n\t\tIsFolder: false,\n\t\tHashInfo: utils.NewHashInfo(utils.MD5, etag),\n\t}, nil\n}\n\nfunc (d *ILanZou) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tres, err := d.proved(\"/user/account/map\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvipSize := utils.Json.Get(res, \"map\", \"vipSize\").ToInt64() * 1024\n\ttotalSize := utils.Json.Get(res, \"map\", \"totalSize\").ToInt64() * 1024\n\trewardSize := utils.Json.Get(res, \"map\", \"rewardSize\").ToInt64() * 1024\n\ttotal := totalSize + rewardSize + vipSize\n\tused := utils.Json.Get(res, \"map\", \"usedSize\").ToInt64() * 1024\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\n//func (d *ILanZou) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*ILanZou)(nil)\n"
  },
  {
    "path": "drivers/ilanzou/meta.go",
    "content": "package template\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername string `json:\"username\" type:\"string\" required:\"true\"`\n\tPassword string `json:\"password\" type:\"string\" required:\"true\"`\n\tIp       string `json:\"ip\" type:\"string\"`\n\n\tToken string\n\tUUID  string\n}\n\ntype Conf struct {\n\tbase       string\n\tsecret     []byte\n\tbucket     string\n\tunproved   string\n\tproved     string\n\tdevVersion string\n\tsite       string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ILanZou{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"ILanZou\",\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tLocalSort:         true,\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tbase:       \"https://api.ilanzou.com\",\n\t\t\t\tsecret:     []byte(\"lanZouY-disk-app\"),\n\t\t\t\tbucket:     \"wpanstore-lanzou\",\n\t\t\t\tunproved:   \"unproved\",\n\t\t\t\tproved:     \"proved\",\n\t\t\t\tdevVersion: \"125\",\n\t\t\t\tsite:       \"https://www.ilanzou.com\",\n\t\t\t},\n\t\t}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ILanZou{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"FeijiPan\",\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tLocalSort:         true,\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tbase:       \"https://api.feijipan.com\",\n\t\t\t\tsecret:     []byte(\"dingHao-disk-app\"),\n\t\t\t\tbucket:     \"wpanstore\",\n\t\t\t\tunproved:   \"ws\",\n\t\t\t\tproved:     \"app\",\n\t\t\t\tdevVersion: \"125\",\n\t\t\t\tsite:       \"https://www.feijipan.com\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/ilanzou/types.go",
    "content": "package template\n\ntype ListResp struct {\n\tMsg       string     `json:\"msg\"`\n\tTotal     int        `json:\"total\"`\n\tCode      int        `json:\"code\"`\n\tOffset    int        `json:\"offset\"`\n\tTotalPage int        `json:\"totalPage\"`\n\tLimit     int        `json:\"limit\"`\n\tList      []ListItem `json:\"list\"`\n}\n\ntype ListItem struct {\n\tIconId         int         `json:\"iconId\"`\n\tIsAmt          int         `json:\"isAmt\"`\n\tFolderDesc     string      `json:\"folderDesc,omitempty\"`\n\tAddTime        string      `json:\"addTime\"`\n\tFolderId       int64       `json:\"folderId\"`\n\tParentId       int64       `json:\"parentId\"`\n\tParentName     string      `json:\"parentName\"`\n\tNoteType       int         `json:\"noteType,omitempty\"`\n\tUpdTime        string      `json:\"updTime\"`\n\tIsShare        int         `json:\"isShare\"`\n\tFolderIcon     string      `json:\"folderIcon,omitempty\"`\n\tFolderName     string      `json:\"folderName,omitempty\"`\n\tFileType       int         `json:\"fileType\"`\n\tStatus         int         `json:\"status\"`\n\tIsFileShare    int         `json:\"isFileShare,omitempty\"`\n\tFileName       string      `json:\"fileName,omitempty\"`\n\tFileStars      float64     `json:\"fileStars,omitempty\"`\n\tIsFileDownload int         `json:\"isFileDownload,omitempty\"`\n\tFileComments   int         `json:\"fileComments,omitempty\"`\n\tFileSize       int64       `json:\"fileSize,omitempty\"`\n\tFileIcon       string      `json:\"fileIcon,omitempty\"`\n\tFileDownloads  int         `json:\"fileDownloads,omitempty\"`\n\tFileUrl        interface{} `json:\"fileUrl\"`\n\tFileLikes      int         `json:\"fileLikes,omitempty\"`\n\tFileId         int64       `json:\"fileId,omitempty\"`\n}\n\ntype Part struct {\n\tPartNumber int    `json:\"partNumber\"`\n\tETag       string `json:\"etag\"`\n}\n\ntype UploadTokenRapidResp struct {\n\tMsg     string `json:\"msg\"`\n\tCode    int    `json:\"code\"`\n\tUpToken string `json:\"upToken\"`\n\tMap     struct {\n\t\tFileIconID int    `json:\"fileIconId\"`\n\t\tFileName   string `json:\"fileName\"`\n\t\tFileIcon   string `json:\"fileIcon\"`\n\t\tFileID     int64  `json:\"fileId\"`\n\t} `json:\"map\"`\n}\n\ntype UploadResultResp struct {\n\tMsg  string `json:\"msg\"`\n\tCode int    `json:\"code\"`\n\tList []struct {\n\t\tFileIconId int    `json:\"fileIconId\"`\n\t\tFileName   string `json:\"fileName\"`\n\t\tFileIcon   string `json:\"fileIcon\"`\n\t\tFileId     int64  `json:\"fileId\"`\n\t\tStatus     int    `json:\"status\"`\n\t\tToken      string `json:\"token\"`\n\t} `json:\"list\"`\n}\n"
  },
  {
    "path": "drivers/ilanzou/util.go",
    "content": "package template\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/foxxorcat/mopan-sdk-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *ILanZou) login() error {\n\tres, err := d.unproved(\"/login\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"loginName\": d.Username,\n\t\t\t\"loginPwd\":  d.Password,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Token = utils.Json.Get(res, \"data\", \"appToken\").ToString()\n\tif d.Token == \"\" {\n\t\treturn fmt.Errorf(\"failed to login: token is empty, resp: %s\", res)\n\t}\n\treturn nil\n}\n\nfunc getTimestamp(secret []byte) (int64, string, error) {\n\tts := time.Now().UnixMilli()\n\ttsStr := strconv.FormatInt(ts, 10)\n\tres, err := mopan.AesEncrypt([]byte(tsStr), secret)\n\tif err != nil {\n\t\treturn 0, \"\", err\n\t}\n\treturn ts, hex.EncodeToString(res), nil\n}\n\nfunc (d *ILanZou) request(pathname, method string, callback base.ReqCallback, proved bool, retry ...bool) ([]byte, error) {\n\t_, ts_str, err := getTimestamp(d.conf.secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparams := []string{\n\t\t\"uuid=\" + url.QueryEscape(d.UUID),\n\t\t\"devType=6\",\n\t\t\"devCode=\" + url.QueryEscape(d.UUID),\n\t\t\"devModel=chrome\",\n\t\t\"devVersion=\" + url.QueryEscape(d.conf.devVersion),\n\t\t\"appVersion=\",\n\t\t\"timestamp=\" + ts_str,\n\t}\n\n\tif proved {\n\t\tparams = append(params, \"appToken=\"+url.QueryEscape(d.Token))\n\t}\n\n\tparams = append(params, \"extra=2\")\n\n\tqueryString := strings.Join(params, \"&\")\n\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Origin\":          d.conf.site,\n\t\t\"Referer\":         d.conf.site + \"/\",\n\t\t\"Accept-Encoding\": \"gzip\",\n\t\t\"Accept-Language\": \"zh-CN,zh;q=0.9,en-US,en;q=0.8\",\n\t})\n\n\tif d.Addition.Ip != \"\" {\n\t\treq.SetHeader(\"X-Forwarded-For\", d.Addition.Ip)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tres, err := req.Execute(method, d.conf.base+pathname+\"?\"+queryString)\n\tif err != nil {\n\t\tif res != nil {\n\t\t\tlog.Errorf(\"[iLanZou] request error: %s\", res.String())\n\t\t}\n\t\treturn nil, err\n\t}\n\tisRetry := len(retry) > 0 && retry[0]\n\tbody := res.Body()\n\tcode := utils.Json.Get(body, \"code\").ToInt()\n\tmsg := utils.Json.Get(body, \"msg\").ToString()\n\tif code != 200 {\n\t\tif !isRetry && proved && (utils.SliceContains([]int{-1, -2}, code) || d.Token == \"\") {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(pathname, method, callback, proved, true)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%d: %s\", code, msg)\n\t}\n\treturn body, nil\n}\n\nfunc (d *ILanZou) unproved(pathname, method string, callback base.ReqCallback) ([]byte, error) {\n\treturn d.request(\"/\"+d.conf.unproved+pathname, method, callback, false)\n}\n\nfunc (d *ILanZou) proved(pathname, method string, callback base.ReqCallback) ([]byte, error) {\n\treturn d.request(\"/\"+d.conf.proved+pathname, method, callback, true)\n}\n"
  },
  {
    "path": "drivers/ipfs_api/driver.go",
    "content": "package ipfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\n\tshell \"github.com/ipfs/go-ipfs-api\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype IPFS struct {\n\tmodel.Storage\n\tAddition\n\tsh      *shell.Shell\n\tgateURL *url.URL\n}\n\nfunc (d *IPFS) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *IPFS) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *IPFS) Init(ctx context.Context) error {\n\td.sh = shell.NewShell(d.Endpoint)\n\tgateURL, err := url.Parse(d.Gateway)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.gateURL = gateURL\n\treturn nil\n}\n\nfunc (d *IPFS) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar ipfsPath string\n\tcid := dir.GetID()\n\tif cid != \"\" {\n\t\tipfsPath = path.Join(\"/ipfs\", cid)\n\t} else {\n\t\t// 可能出现ipns dns解析失败的情况，需要重复获取cid，其他情况应该不会出错\n\t\tipfsPath = dir.GetPath()\n\t\tswitch d.Mode {\n\t\tcase \"ipfs\":\n\t\t\tipfsPath = path.Join(\"/ipfs\", ipfsPath)\n\t\tcase \"ipns\":\n\t\t\tipfsPath = path.Join(\"/ipns\", ipfsPath)\n\t\tcase \"mfs\":\n\t\t\tfileStat, err := d.sh.FilesStat(ctx, ipfsPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tipfsPath = path.Join(\"/ipfs\", fileStat.Hash)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"mode error\")\n\t\t}\n\t}\n\tdirs, err := d.sh.List(ipfsPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tobjlist := []model.Obj{}\n\tfor _, file := range dirs {\n\t\tobjlist = append(objlist, &model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1})\n\t}\n\n\treturn objlist, nil\n}\n\nfunc (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tgateurl := d.gateURL.JoinPath(\"/ipfs/\", file.GetID())\n\tgateurl.RawQuery = \"filename=\" + url.QueryEscape(file.GetName())\n\treturn &model.Link{URL: gateurl.String()}, nil\n}\n\nfunc (d *IPFS) Get(ctx context.Context, rawPath string) (model.Obj, error) {\n\trawPath = path.Join(d.GetRootPath(), rawPath)\n\tvar ipfsPath string\n\tswitch d.Mode {\n\tcase \"ipfs\":\n\t\tipfsPath = path.Join(\"/ipfs\", rawPath)\n\tcase \"ipns\":\n\t\tipfsPath = path.Join(\"/ipns\", rawPath)\n\tcase \"mfs\":\n\t\tfileStat, err := d.sh.FilesStat(ctx, rawPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tipfsPath = path.Join(\"/ipfs\", fileStat.Hash)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"mode error\")\n\t}\n\tfile, err := d.sh.FilesStat(ctx, ipfsPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{ID: file.Hash, Name: path.Base(rawPath), Path: rawPath, Size: int64(file.Size), IsFolder: file.Type == \"directory\"}, nil\n}\n\nfunc (d *IPFS) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif d.Mode != \"mfs\" {\n\t\treturn nil, fmt.Errorf(\"only write in mfs mode\")\n\t}\n\tdirPath := parentDir.GetPath()\n\terr := d.sh.FilesMkdir(ctx, path.Join(dirPath, dirName), shell.FilesMkdir.Parents(true))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfile, err := d.sh.FilesStat(ctx, path.Join(dirPath, dirName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{ID: file.Hash, Name: dirName, Path: path.Join(dirPath, dirName), Size: int64(file.Size), IsFolder: true}, nil\n}\n\nfunc (d *IPFS) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif d.Mode != \"mfs\" {\n\t\treturn nil, fmt.Errorf(\"only write in mfs mode\")\n\t}\n\tdstPath := path.Join(dstDir.GetPath(), path.Base(srcObj.GetPath()))\n\td.sh.FilesRm(ctx, dstPath, true)\n\treturn &model.Object{ID: srcObj.GetID(), Name: srcObj.GetName(), Path: dstPath, Size: int64(srcObj.GetSize()), IsFolder: srcObj.IsDir()},\n\t\td.sh.FilesMv(ctx, srcObj.GetPath(), dstDir.GetPath())\n}\n\nfunc (d *IPFS) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif d.Mode != \"mfs\" {\n\t\treturn nil, fmt.Errorf(\"only write in mfs mode\")\n\t}\n\tdstPath := path.Join(path.Dir(srcObj.GetPath()), newName)\n\td.sh.FilesRm(ctx, dstPath, true)\n\treturn &model.Object{ID: srcObj.GetID(), Name: newName, Path: dstPath, Size: int64(srcObj.GetSize()),\n\t\tIsFolder: srcObj.IsDir()}, d.sh.FilesMv(ctx, srcObj.GetPath(), dstPath)\n}\n\nfunc (d *IPFS) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif d.Mode != \"mfs\" {\n\t\treturn nil, fmt.Errorf(\"only write in mfs mode\")\n\t}\n\tdstPath := path.Join(dstDir.GetPath(), path.Base(srcObj.GetPath()))\n\td.sh.FilesRm(ctx, dstPath, true)\n\treturn &model.Object{ID: srcObj.GetID(), Name: srcObj.GetName(), Path: dstPath, Size: int64(srcObj.GetSize()), IsFolder: srcObj.IsDir()},\n\t\td.sh.FilesCp(ctx, path.Join(\"/ipfs/\", srcObj.GetID()), dstPath, shell.FilesCp.Parents(true))\n}\n\nfunc (d *IPFS) Remove(ctx context.Context, obj model.Obj) error {\n\tif d.Mode != \"mfs\" {\n\t\treturn fmt.Errorf(\"only write in mfs mode\")\n\t}\n\treturn d.sh.FilesRm(ctx, obj.GetPath(), true)\n}\n\nfunc (d *IPFS) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif d.Mode != \"mfs\" {\n\t\treturn nil, fmt.Errorf(\"only write in mfs mode\")\n\t}\n\toutHash, err := d.sh.Add(driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdstPath := path.Join(dstDir.GetPath(), s.GetName())\n\tif s.GetExist() != nil {\n\t\td.sh.FilesRm(ctx, dstPath, true)\n\t}\n\terr = d.sh.FilesCp(ctx, path.Join(\"/ipfs/\", outHash), dstPath, shell.FilesCp.Parents(true))\n\tgateurl := d.gateURL.JoinPath(\"/ipfs/\", outHash)\n\tgateurl.RawQuery = \"filename=\" + url.QueryEscape(s.GetName())\n\treturn &model.Object{ID: outHash, Name: s.GetName(), Path: dstPath, Size: int64(s.GetSize()), IsFolder: s.IsDir()}, err\n}\n\n//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*IPFS)(nil)\n"
  },
  {
    "path": "drivers/ipfs_api/meta.go",
    "content": "package ipfs\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\tMode     string `json:\"mode\" options:\"ipfs,ipns,mfs\" type:\"select\" required:\"true\"`\n\tEndpoint string `json:\"endpoint\" default:\"http://127.0.0.1:5001\" required:\"true\"`\n\tGateway  string `json:\"gateway\" default:\"http://127.0.0.1:8080\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:        \"IPFS API\",\n\tDefaultRoot: \"/\",\n\tLocalSort:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &IPFS{}\n\t})\n}\n"
  },
  {
    "path": "drivers/kodbox/driver.go",
    "content": "package kodbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype KodBox struct {\n\tmodel.Storage\n\tAddition\n\tauthorization string\n}\n\nfunc (d *KodBox) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *KodBox) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *KodBox) Init(ctx context.Context) error {\n\td.Address = strings.TrimSuffix(d.Address, \"/\")\n\td.RootFolderPath = strings.TrimPrefix(utils.FixAndCleanPath(d.RootFolderPath), \"/\")\n\treturn d.getToken()\n}\n\nfunc (d *KodBox) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *KodBox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar (\n\t\tresp         *CommonResp\n\t\tlistPathData *ListPathData\n\t)\n\n\t_, err := d.request(http.MethodPost, \"/?explorer/list/path\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"path\": dir.GetPath(),\n\t\t})\n\t}, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdataBytes, err := utils.Json.Marshal(resp.Data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = utils.Json.Unmarshal(dataBytes, &listPathData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tFolderAndFiles := append(listPathData.FolderList, listPathData.FileList...)\n\n\treturn utils.SliceConvert(FolderAndFiles, func(f FolderOrFile) (model.Obj, error) {\n\t\treturn &model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tPath:     f.Path,\n\t\t\t\tName:     f.Name,\n\t\t\t\tCtime:    time.Unix(f.CreateTime, 0),\n\t\t\t\tModified: time.Unix(f.ModifyTime, 0),\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.Type == \"folder\",\n\t\t\t},\n\t\t\t//Thumbnail: model.Thumbnail{},\n\t\t}, nil\n\t})\n}\n\nfunc (d *KodBox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tpath := file.GetPath()\n\treturn &model.Link{\n\t\tURL: fmt.Sprintf(\"%s/?explorer/index/fileOut&path=%s&download=1&accessToken=%s\",\n\t\t\td.Address,\n\t\t\tpath,\n\t\t\td.authorization)}, nil\n}\n\nfunc (d *KodBox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar resp *CommonResp\n\tnewDirPath := filepath.Join(parentDir.GetPath(), dirName)\n\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/mkdir\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"path\": newDirPath,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     resp.Info.(string),\n\t\t\tName:     dirName,\n\t\t\tIsFolder: true,\n\t\t\tModified: time.Now(),\n\t\t\tCtime:    time.Now(),\n\t\t},\n\t}, nil\n}\n\nfunc (d *KodBox) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/pathCuteTo\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"dataArr\": fmt.Sprintf(\"[{\\\"path\\\": \\\"%s\\\", \\\"name\\\": \\\"%s\\\"}]\",\n\t\t\t\tsrcObj.GetPath(),\n\t\t\t\tsrcObj.GetName()),\n\t\t\t\"path\": dstDir.GetPath(),\n\t\t})\n\t}, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     srcObj.GetPath(),\n\t\t\tName:     srcObj.GetName(),\n\t\t\tIsFolder: srcObj.IsDir(),\n\t\t\tModified: srcObj.ModTime(),\n\t\t\tCtime:    srcObj.CreateTime(),\n\t\t},\n\t}, nil\n}\n\nfunc (d *KodBox) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/pathRename\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"newName\": newName,\n\t\t})\n\t}, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     srcObj.GetPath(),\n\t\t\tName:     newName,\n\t\t\tIsFolder: srcObj.IsDir(),\n\t\t\tModified: time.Now(),\n\t\t\tCtime:    srcObj.CreateTime(),\n\t\t},\n\t}, nil\n}\n\nfunc (d *KodBox) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/pathCopyTo\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"dataArr\": fmt.Sprintf(\"[{\\\"path\\\": \\\"%s\\\", \\\"name\\\": \\\"%s\\\"}]\",\n\t\t\t\tsrcObj.GetPath(),\n\t\t\t\tsrcObj.GetName()),\n\t\t\t\"path\": dstDir.GetPath(),\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\n\tpath := resp.Info.([]interface{})[0].(string)\n\tobjectName, err := d.getFileOrFolderName(ctx, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     path,\n\t\t\tName:     *objectName,\n\t\t\tIsFolder: srcObj.IsDir(),\n\t\t\tModified: time.Now(),\n\t\t\tCtime:    time.Now(),\n\t\t},\n\t}, nil\n}\n\nfunc (d *KodBox) Remove(ctx context.Context, obj model.Obj) error {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/pathDelete\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"dataArr\": fmt.Sprintf(\"[{\\\"path\\\": \\\"%s\\\", \\\"name\\\": \\\"%s\\\"}]\",\n\t\t\t\tobj.GetPath(),\n\t\t\t\tobj.GetName()),\n\t\t\t\"shiftDelete\": \"1\",\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn fmt.Errorf(\"%s\", resp.Data)\n\t}\n\treturn nil\n}\n\nfunc (d *KodBox) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/upload/fileUpload\", func(req *resty.Request) {\n\t\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader:         s,\n\t\t\tUpdateProgress: up,\n\t\t})\n\t\treq.SetFileReader(\"file\", s.GetName(), r).\n\t\t\tSetResult(&resp).\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"path\": dstDir.GetPath(),\n\t\t\t}).\n\t\t\tSetContext(ctx)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     resp.Info.(string),\n\t\t\tName:     s.GetName(),\n\t\t\tSize:     s.GetSize(),\n\t\t\tIsFolder: false,\n\t\t\tModified: time.Now(),\n\t\t\tCtime:    time.Now(),\n\t\t},\n\t}, nil\n}\n\nfunc (d *KodBox) getFileOrFolderName(ctx context.Context, path string) (*string, error) {\n\tvar resp *CommonResp\n\t_, err := d.request(http.MethodPost, \"/?explorer/index/pathInfo\", func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"dataArr\": fmt.Sprintf(\"[{\\\"path\\\": \\\"%s\\\"}]\", path)})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := resp.Code.(bool)\n\tif !code {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Data)\n\t}\n\tfolderOrFileName := resp.Data.(map[string]any)[\"name\"].(string)\n\treturn &folderOrFileName, nil\n}\n\nvar _ driver.Driver = (*KodBox)(nil)\n"
  },
  {
    "path": "drivers/kodbox/meta.go",
    "content": "package kodbox\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\n\tAddress  string `json:\"address\" required:\"true\"`\n\tUserName string `json:\"username\" required:\"false\"`\n\tPassword string `json:\"password\" required:\"false\"`\n}\n\nvar config = driver.Config{\n\tName: \"KodBox\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &KodBox{}\n\t})\n}\n"
  },
  {
    "path": "drivers/kodbox/types.go",
    "content": "package kodbox\n\ntype CommonResp struct {\n\tCode    any    `json:\"code\"`\n\tTimeUse string `json:\"timeUse\"`\n\tTimeNow string `json:\"timeNow\"`\n\tData    any    `json:\"data\"`\n\tInfo    any    `json:\"info\"`\n}\n\ntype ListPathData struct {\n\tFolderList []FolderOrFile `json:\"folderList\"`\n\tFileList   []FolderOrFile `json:\"fileList\"`\n}\n\ntype FolderOrFile struct {\n\tName       string `json:\"name\"`\n\tPath       string `json:\"path\"`\n\tType       string `json:\"type\"`\n\tExt        string `json:\"ext,omitempty\"` // 文件特有字段\n\tSize       int64  `json:\"size\"`\n\tCreateTime int64  `json:\"createTime\"`\n\tModifyTime int64  `json:\"modifyTime\"`\n}\n"
  },
  {
    "path": "drivers/kodbox/util.go",
    "content": "package kodbox\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *KodBox) getToken() error {\n\tvar authResp CommonResp\n\tres, err := base.RestyClient.R().\n\t\tSetResult(&authResp).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"name\":     d.UserName,\n\t\t\t\"password\": d.Password,\n\t\t}).\n\t\tPost(d.Address + \"/?user/index/loginSubmit\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() >= 400 {\n\t\treturn fmt.Errorf(\"get token failed: %s\", res.String())\n\t}\n\n\tif res.StatusCode() == 200 && authResp.Code.(bool) == false {\n\t\treturn fmt.Errorf(\"get token failed: %s\", res.String())\n\t}\n\n\td.authorization = fmt.Sprintf(\"%s\", authResp.Info)\n\treturn nil\n}\n\nfunc (d *KodBox) request(method string, pathname string, callback base.ReqCallback, noRedirect ...bool) ([]byte, error) {\n\tfull := pathname\n\tif !strings.HasPrefix(pathname, \"http\") {\n\t\tfull = d.Address + pathname\n\t}\n\treq := base.RestyClient.R()\n\tif len(noRedirect) > 0 && noRedirect[0] {\n\t\treq = base.NoRedirectClient.R()\n\t}\n\treq.SetFormData(map[string]string{\n\t\t\"accessToken\": d.authorization,\n\t})\n\tcallback(req)\n\n\tvar (\n\t\tres        *resty.Response\n\t\tcommonResp *CommonResp\n\t\terr        error\n\t\tskip       bool\n\t)\n\tfor i := 0; i < 2; i++ {\n\t\tif skip {\n\t\t\tbreak\n\t\t}\n\t\tres, err = req.Execute(method, full)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr := utils.Json.Unmarshal(res.Body(), &commonResp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch commonResp.Code.(type) {\n\t\tcase bool:\n\t\t\tskip = true\n\t\tcase string:\n\t\t\tif commonResp.Code.(string) == \"10001\" {\n\t\t\t\terr = d.getToken()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treq.SetFormData(map[string]string{\"accessToken\": d.authorization})\n\t\t\t}\n\t\t}\n\t}\n\tif commonResp.Code.(bool) == false {\n\t\treturn nil, fmt.Errorf(\"request failed: %s\", commonResp.Data)\n\t}\n\treturn res.Body(), nil\n}\n"
  },
  {
    "path": "drivers/lanzou/driver.go",
    "content": "package lanzou\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype LanZou struct {\n\tAddition\n\tmodel.Storage\n\tuid string\n\tvei string\n\n\tflag int32\n}\n\nfunc (d *LanZou) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *LanZou) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *LanZou) Init(ctx context.Context) (err error) {\n\tif d.UserAgent == \"\" {\n\t\td.UserAgent = base.UserAgentNT\n\t}\n\tswitch d.Type {\n\tcase \"account\":\n\t\t_, err := d.Login()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfallthrough\n\tcase \"cookie\":\n\t\tif d.RootFolderID == \"\" {\n\t\t\td.RootFolderID = \"-1\"\n\t\t}\n\t\td.vei, d.uid, err = d.getVeiAndUid()\n\t}\n\treturn\n}\n\nfunc (d *LanZou) Drop(ctx context.Context) error {\n\td.uid = \"\"\n\treturn nil\n}\n\n// 获取的大小和时间不准确\nfunc (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif d.IsCookie() || d.IsAccount() {\n\t\treturn d.GetAllFiles(dir.GetID())\n\t} else {\n\t\treturn d.GetFileOrFolderByShareUrl(dir.GetID(), d.SharePassword)\n\t}\n}\n\nfunc (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar (\n\t\terr   error\n\t\tdfile *FileOrFolderByShareUrl\n\t)\n\tswitch file := file.(type) {\n\tcase *FileOrFolder:\n\t\t// 先获取分享链接\n\t\tsfile := file.GetShareInfo()\n\t\tif sfile == nil {\n\t\t\tsfile, err = d.getFileShareUrlByID(file.GetID())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfile.SetShareInfo(sfile)\n\t\t}\n\n\t\t// 然后获取下载链接\n\t\tdfile, err = d.GetFilesByShareUrl(sfile.FID, sfile.Pwd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 修复文件大小\n\t\tif d.RepairFileInfo && !file.repairFlag {\n\t\t\tsize, time := d.getFileRealInfo(dfile.Url)\n\t\t\tif size != nil {\n\t\t\t\tfile.size = size\n\t\t\t\tfile.repairFlag = true\n\t\t\t}\n\t\t\tif file.time != nil {\n\t\t\t\tfile.time = time\n\t\t\t}\n\t\t}\n\tcase *FileOrFolderByShareUrl:\n\t\tdfile, err = d.GetFilesByShareUrl(file.GetID(), file.Pwd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 修复文件大小\n\t\tif d.RepairFileInfo && !file.repairFlag {\n\t\t\tsize, time := d.getFileRealInfo(dfile.Url)\n\t\t\tif size != nil {\n\t\t\t\tfile.size = size\n\t\t\t\tfile.repairFlag = true\n\t\t\t}\n\t\t\tif file.time != nil {\n\t\t\t\tfile.time = time\n\t\t\t}\n\t\t}\n\t}\n\texp := GetExpirationTime(dfile.Url)\n\treturn &model.Link{\n\t\tURL: dfile.Url,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{base.UserAgent},\n\t\t},\n\t\tExpiration: &exp,\n\t}, nil\n}\n\nfunc (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif d.IsCookie() || d.IsAccount() {\n\t\tdata, err := d.doupload(func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"task\":               \"2\",\n\t\t\t\t\"parent_id\":          parentDir.GetID(),\n\t\t\t\t\"folder_name\":        dirName,\n\t\t\t\t\"folder_description\": \"\",\n\t\t\t})\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &FileOrFolder{\n\t\t\tName:  dirName,\n\t\t\tFolID: utils.Json.Get(data, \"text\").ToString(),\n\t\t}, nil\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif d.IsCookie() || d.IsAccount() {\n\t\tif !srcObj.IsDir() {\n\t\t\t_, err := d.doupload(func(req *resty.Request) {\n\t\t\t\treq.SetContext(ctx)\n\t\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\t\"task\":      \"20\",\n\t\t\t\t\t\"folder_id\": dstDir.GetID(),\n\t\t\t\t\t\"file_id\":   srcObj.GetID(),\n\t\t\t\t})\n\t\t\t}, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn srcObj, nil\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif d.IsCookie() || d.IsAccount() {\n\t\tif !srcObj.IsDir() {\n\t\t\t_, err := d.doupload(func(req *resty.Request) {\n\t\t\t\treq.SetContext(ctx)\n\t\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\t\"task\":      \"46\",\n\t\t\t\t\t\"file_id\":   srcObj.GetID(),\n\t\t\t\t\t\"file_name\": newName,\n\t\t\t\t\t\"type\":      \"2\",\n\t\t\t\t})\n\t\t\t}, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsrcObj.(*FileOrFolder).NameAll = newName\n\t\t\treturn srcObj, nil\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {\n\tif d.IsCookie() || d.IsAccount() {\n\t\t_, err := d.doupload(func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t\tif obj.IsDir() {\n\t\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\t\"task\":      \"3\",\n\t\t\t\t\t\"folder_id\": obj.GetID(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\t\"task\":    \"6\",\n\t\t\t\t\t\"file_id\": obj.GetID(),\n\t\t\t\t})\n\t\t\t}\n\t\t}, nil)\n\t\treturn err\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *LanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif d.IsCookie() || d.IsAccount() {\n\t\tvar resp RespText[[]FileOrFolder]\n\t\t_, err := d._post(d.BaseUrl+\"/html5up.php\", func(req *resty.Request) {\n\t\t\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\t\tReader:         s,\n\t\t\t\tUpdateProgress: up,\n\t\t\t})\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"task\":           \"1\",\n\t\t\t\t\"vie\":            \"2\",\n\t\t\t\t\"ve\":             \"2\",\n\t\t\t\t\"id\":             \"WU_FILE_0\",\n\t\t\t\t\"name\":           s.GetName(),\n\t\t\t\t\"folder_id_bb_n\": dstDir.GetID(),\n\t\t\t}).SetFileReader(\"upload_file\", s.GetName(), reader).SetContext(ctx)\n\t\t}, &resp, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &resp.Text[0], nil\n\t}\n\treturn nil, errs.NotSupport\n}\n"
  },
  {
    "path": "drivers/lanzou/help.go",
    "content": "package lanzou\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n)\n\nconst DAY time.Duration = 84600000000000\n\n// 解析时间\nvar timeSplitReg = regexp.MustCompile(\"([0-9.]*)\\\\s*([\\u4e00-\\u9fa5]+)\")\n\n// 如果解析失败,则返回当前时间\nfunc MustParseTime(str string) time.Time {\n\tlastOpTime, err := time.ParseInLocation(\"2006-01-02 -07\", str+\" +08\", time.Local)\n\tif err != nil {\n\t\tstrs := timeSplitReg.FindStringSubmatch(str)\n\t\tlastOpTime = time.Now()\n\t\tif len(strs) == 3 {\n\t\t\ti, _ := strconv.ParseInt(strs[1], 10, 64)\n\t\t\tti := time.Duration(-i)\n\t\t\tswitch strs[2] {\n\t\t\tcase \"秒前\":\n\t\t\t\tlastOpTime = lastOpTime.Add(time.Second * ti)\n\t\t\tcase \"分钟前\":\n\t\t\t\tlastOpTime = lastOpTime.Add(time.Minute * ti)\n\t\t\tcase \"小时前\":\n\t\t\t\tlastOpTime = lastOpTime.Add(time.Hour * ti)\n\t\t\tcase \"天前\":\n\t\t\t\tlastOpTime = lastOpTime.Add(DAY * ti)\n\t\t\tcase \"昨天\":\n\t\t\t\tlastOpTime = lastOpTime.Add(-DAY)\n\t\t\tcase \"前天\":\n\t\t\t\tlastOpTime = lastOpTime.Add(-DAY * 2)\n\t\t\t}\n\t\t}\n\t}\n\treturn lastOpTime\n}\n\n// 解析大小\nvar sizeSplitReg = regexp.MustCompile(`(?i)([0-9.]+)\\s*([bkm]+)`)\n\n// 解析失败返回0\nfunc SizeStrToInt64(size string) int64 {\n\tstrs := sizeSplitReg.FindStringSubmatch(size)\n\tif len(strs) < 3 {\n\t\treturn 0\n\t}\n\n\ts, _ := strconv.ParseFloat(strs[1], 64)\n\tswitch strings.ToUpper(strs[2]) {\n\tcase \"B\":\n\t\treturn int64(s)\n\tcase \"K\":\n\t\treturn int64(s * (1 << 10))\n\tcase \"M\":\n\t\treturn int64(s * (1 << 20))\n\t}\n\treturn 0\n}\n\n// 移除注释\nfunc RemoveNotes(html string) string {\n\treturn regexp.MustCompile(`<!--.*?-->|[^:]//.*|/\\*.*?\\*/`).ReplaceAllStringFunc(html, func(b string) string {\n\t\tif b[1:3] == \"//\" {\n\t\t\treturn b[:1]\n\t\t}\n\t\treturn \"\\n\"\n\t})\n}\n\n// 清理JS注释\nfunc RemoveJSComment(data string) string {\n\tvar result strings.Builder\n\tinComment := false\n\tinSingleLineComment := false\n\n\tfor i := 0; i < len(data); i++ {\n\t\tv := data[i]\n\n\t\tif inSingleLineComment && (v == '\\n' || v == '\\r') {\n\t\t\tinSingleLineComment = false\n\t\t\tresult.WriteByte(v)\n\t\t\tcontinue\n\t\t}\n\t\tif inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {\n\t\t\tinComment = false\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif inComment || inSingleLineComment {\n\t\t\tcontinue\n\t\t}\n\t\tif v == '/' && i+1 < len(data) {\n\t\t\tnextChar := data[i+1]\n\t\t\tif nextChar == '*' {\n\t\t\t\tinComment = true\n\t\t\t\ti++\n\t\t\t\tcontinue\n\t\t\t} else if nextChar == '/' {\n\t\t\t\tinSingleLineComment = true\n\t\t\t\ti++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tresult.WriteByte(v)\n\t}\n\n\treturn result.String()\n}\n\nvar findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)\n\n// 在页面被过多访问或其他情况下，有时候会先返回一个加密的页面，其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面\n// 若该页面进行了js加密，则进行解密，计算acw_sc__v2，并加入cookie\nfunc CalcAcwScV2(htmlContent string) (string, error) {\n\tmatches := findAcwScV2Reg.FindStringSubmatch(htmlContent)\n\tif len(matches) != 2 {\n\t\treturn \"\", errors.New(\"无法匹配到 arg1 参数\")\n\t}\n\targ1 := matches[1]\n\n\tmask := \"3000176000856006061501533003690027800375\"\n\tresult, err := hexXor(unbox(arg1), mask)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"hexXor 操作失败: %w\", err)\n\t}\n\n\treturn result, nil\n}\n\nfunc unbox(hex string) string {\n\tvar box = []int{6, 28, 34, 31, 33, 18, 30, 23, 9, 8, 19, 38, 17, 24, 0, 5, 32, 21, 10, 22, 25, 14, 15, 3, 16, 27, 13, 35, 2, 29, 11, 26, 4, 36, 1, 39, 37, 7, 20, 12}\n\tvar newBox = make([]byte, len(hex))\n\tfor i, j := range box {\n\t\tif len(newBox) > j {\n\t\t\tnewBox[j] = hex[i]\n\t\t}\n\t}\n\treturn string(newBox)\n}\n\nfunc hexXor(hex1, hex2 string) (string, error) {\n\tbytes1, err := hex.DecodeString(hex1)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"解码 hex1 失败: %w\", err)\n\t}\n\tbytes2, err := hex.DecodeString(hex2)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"解码 hex2 失败: %w\", err)\n\t}\n\tminLength := min(len(bytes2), len(bytes1))\n\tresultBytes := make([]byte, minLength)\n\tfor i := range minLength {\n\t\tresultBytes[i] = bytes1[i] ^ bytes2[i]\n\t}\n\treturn hex.EncodeToString(resultBytes), nil\n}\n\nvar findDataReg = regexp.MustCompile(`data[:\\s]+({[^}]+})`)    // 查找json\nvar findKVReg = regexp.MustCompile(`'(.+?)':('?([^' },]*)'?)`) // 拆分kv\n\n// 根据key查询js变量\nfunc findJSVarFunc(key, data string) string {\n\tvar values []string\n\tif key != \"sasign\" {\n\t\tvalues = regexp.MustCompile(`var ` + key + `\\s*=\\s*['\"]?(.+?)['\"]?;`).FindStringSubmatch(data)\n\t} else {\n\t\tmatches := regexp.MustCompile(`var `+key+`\\s*=\\s*['\"]?(.+?)['\"]?;`).FindAllStringSubmatch(data, -1)\n\t\tif len(matches) == 3 {\n\t\t\tvalues = matches[1]\n\t\t} else {\n\t\t\tif len(matches) > 0 {\n\t\t\t\tvalues = matches[0]\n\t\t\t}\n\t\t}\n\t}\n\tif len(values) == 0 {\n\t\treturn \"\"\n\t}\n\treturn values[1]\n}\n\nvar findFunction = regexp.MustCompile(`(?ims)^function[^{]+`)\nvar findFunctionAll = regexp.MustCompile(`(?is)function[^{]+`)\n\n// 查找所有方法位置\nfunc findJSFunctionIndex(data string, all bool) [][2]int {\n\tfindFunction := findFunction\n\tif all {\n\t\tfindFunction = findFunctionAll\n\t}\n\n\tindexs := findFunction.FindAllStringIndex(data, -1)\n\tfIndexs := make([][2]int, 0, len(indexs))\n\n\tfor _, index := range indexs {\n\t\tif len(index) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tcount, data := 0, data[index[1]:]\n\t\tfor ii, v := range data {\n\t\t\tif v == ' ' && count == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif v == '{' {\n\t\t\t\tcount++\n\t\t\t}\n\n\t\t\tif v == '}' {\n\t\t\t\tcount--\n\t\t\t}\n\t\t\tif count == 0 {\n\t\t\t\tfIndexs = append(fIndexs, [2]int{index[0], index[1] + ii + 1})\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn fIndexs\n}\n\n// 删除JS全局方法\nfunc removeJSGlobalFunction(html string) string {\n\tindexs := findJSFunctionIndex(html, false)\n\tblock := make([]string, len(indexs))\n\tfor i, next := len(indexs)-1, len(html); i >= 0; i-- {\n\t\tindex := indexs[i]\n\t\tblock[i] = html[index[1]:next]\n\t\tnext = index[0]\n\t}\n\treturn strings.Join(block, \"\")\n}\n\n// 根据名称获取方法\nfunc getJSFunctionByName(html string, name string) (string, error) {\n\tindexs := findJSFunctionIndex(html, true)\n\tfor _, index := range indexs {\n\t\tdata := html[index[0]:index[1]]\n\t\tif regexp.MustCompile(`function\\s+` + name + `[()\\s]+{`).MatchString(data) {\n\t\t\treturn data, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"not find %s function\", name)\n}\n\n// 解析html中的JSON,选择最长的数据\nfunc htmlJsonToMap2(html string) (map[string]string, error) {\n\tdatas := findDataReg.FindAllStringSubmatch(html, -1)\n\tvar sData string\n\tfor _, data := range datas {\n\t\tif len(datas) > 0 && len(data[1]) > len(sData) {\n\t\t\tsData = data[1]\n\t\t}\n\t}\n\tif sData == \"\" {\n\t\treturn nil, fmt.Errorf(\"not find data\")\n\t}\n\treturn jsonToMap(sData, html), nil\n}\n\n// 解析html中的JSON\nfunc htmlJsonToMap(html string) (map[string]string, error) {\n\tdatas := findDataReg.FindStringSubmatch(html)\n\tif len(datas) != 2 {\n\t\treturn nil, fmt.Errorf(\"not find data\")\n\t}\n\treturn jsonToMap(datas[1], html), nil\n}\n\nfunc jsonToMap(data, html string) map[string]string {\n\tvar param = make(map[string]string)\n\tkvs := findKVReg.FindAllStringSubmatch(data, -1)\n\tfor _, kv := range kvs {\n\t\tk, v := kv[1], kv[3]\n\t\tif v == \"\" || strings.Contains(kv[2], \"'\") || IsNumber(kv[2]) {\n\t\t\tparam[k] = v\n\t\t} else {\n\t\t\tparam[k] = findJSVarFunc(v, html)\n\t\t}\n\t}\n\treturn param\n}\n\nfunc IsNumber(str string) bool {\n\tfor _, s := range str {\n\t\tif !unicode.IsDigit(s) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nvar findFromReg = regexp.MustCompile(`data : '(.+?)'`) // 查找from字符串\n\n// 解析html中的form\nfunc htmlFormToMap(html string) (map[string]string, error) {\n\tforms := findFromReg.FindStringSubmatch(html)\n\tif len(forms) != 2 {\n\t\treturn nil, fmt.Errorf(\"not find file sgin\")\n\t}\n\treturn formToMap(forms[1]), nil\n}\n\nfunc formToMap(from string) map[string]string {\n\tvar param = make(map[string]string)\n\tfor _, kv := range strings.Split(from, \"&\") {\n\t\tkv := strings.SplitN(kv, \"=\", 2)[:2]\n\t\tparam[kv[0]] = kv[1]\n\t}\n\treturn param\n}\n\nvar regExpirationTime = regexp.MustCompile(`e=(\\d+)`)\n\nfunc GetExpirationTime(url string) (etime time.Duration) {\n\texps := regExpirationTime.FindStringSubmatch(url)\n\tif len(exps) < 2 {\n\t\treturn\n\t}\n\ttimestamp, err := strconv.ParseInt(exps[1], 10, 64)\n\tif err != nil {\n\t\treturn\n\t}\n\tetime = time.Duration(timestamp-time.Now().Unix()) * time.Second\n\treturn\n}\n\nfunc CookieToString(cookies []*http.Cookie) string {\n\tif cookies == nil {\n\t\treturn \"\"\n\t}\n\tcookieStrings := make([]string, len(cookies))\n\tfor i, cookie := range cookies {\n\t\tcookieStrings[i] = cookie.Name + \"=\" + cookie.Value\n\t}\n\treturn strings.Join(cookieStrings, \";\")\n}\n"
  },
  {
    "path": "drivers/lanzou/meta.go",
    "content": "package lanzou\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tType string `json:\"type\" type:\"select\" options:\"account,cookie,url\" default:\"cookie\"`\n\n\tAccount  string `json:\"account\"`\n\tPassword string `json:\"password\"`\n\n\tCookie string `json:\"cookie\" help:\"about 15 days valid, ignore if shareUrl is used\"`\n\n\tdriver.RootID\n\tSharePassword  string `json:\"share_password\"`\n\tBaseUrl        string `json:\"baseUrl\" required:\"true\" default:\"https://pc.woozooo.com\" help:\"basic URL for file operation\"`\n\tShareUrl       string `json:\"shareUrl\" required:\"true\" default:\"https://pan.lanzoui.com\" help:\"used to get the sharing page\"`\n\tUserAgent      string `json:\"user_agent\" required:\"true\" default:\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.39 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.39\"`\n\tRepairFileInfo bool   `json:\"repair_file_info\" help:\"To use webdav, you need to enable it\"`\n}\n\nfunc (a *Addition) IsCookie() bool {\n\treturn a.Type == \"cookie\"\n}\n\nfunc (a *Addition) IsAccount() bool {\n\treturn a.Type == \"account\"\n}\n\nvar config = driver.Config{\n\tName:        \"Lanzou\",\n\tLocalSort:   true,\n\tDefaultRoot: \"-1\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &LanZou{}\n\t})\n}\n"
  },
  {
    "path": "drivers/lanzou/types.go",
    "content": "package lanzou\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nvar ErrFileShareCancel = errors.New(\"file sharing cancellation\")\nvar ErrFileNotExist = errors.New(\"file does not exist\")\nvar ErrCookieExpiration = errors.New(\"cookie expiration\")\n\ntype RespText[T any] struct {\n\tText T `json:\"text\"`\n}\n\ntype RespInfo[T any] struct {\n\tInfo T `json:\"info\"`\n}\n\nvar _ model.Obj = (*FileOrFolder)(nil)\nvar _ model.Obj = (*FileOrFolderByShareUrl)(nil)\n\ntype FileOrFolder struct {\n\tName string `json:\"name\"`\n\t//Onof        string `json:\"onof\"` // 是否存在提取码\n\t//IsLock      string `json:\"is_lock\"`\n\t//IsCopyright int    `json:\"is_copyright\"`\n\n\t// 文件通用\n\tID      string `json:\"id\"`\n\tNameAll string `json:\"name_all\"`\n\tSize    string `json:\"size\"`\n\tTime    string `json:\"time\"`\n\t//Icon          string `json:\"icon\"`\n\t//Downs         string `json:\"downs\"`\n\t//Filelock      string `json:\"filelock\"`\n\t//IsBakdownload int    `json:\"is_bakdownload\"`\n\t//Bakdownload   string `json:\"bakdownload\"`\n\t//IsDes         int    `json:\"is_des\"` // 是否存在描述\n\t//IsIco         int    `json:\"is_ico\"`\n\n\t// 文件夹\n\tFolID string `json:\"fol_id\"`\n\t//Folderlock string `json:\"folderlock\"`\n\t//FolderDes  string `json:\"folder_des\"`\n\n\t// 缓存字段\n\tsize       *int64     `json:\"-\"`\n\ttime       *time.Time `json:\"-\"`\n\trepairFlag bool       `json:\"-\"`\n\tshareInfo  *FileShare `json:\"-\"`\n}\n\nfunc (f *FileOrFolder) CreateTime() time.Time {\n\treturn f.ModTime()\n}\n\nfunc (f *FileOrFolder) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *FileOrFolder) GetID() string {\n\tif f.IsDir() {\n\t\treturn f.FolID\n\t}\n\treturn f.ID\n}\nfunc (f *FileOrFolder) GetName() string {\n\tif f.IsDir() {\n\t\treturn f.Name\n\t}\n\treturn f.NameAll\n}\nfunc (f *FileOrFolder) GetPath() string { return \"\" }\nfunc (f *FileOrFolder) GetSize() int64 {\n\tif f.size == nil {\n\t\tsize := SizeStrToInt64(f.Size)\n\t\tf.size = &size\n\t}\n\treturn *f.size\n}\nfunc (f *FileOrFolder) IsDir() bool { return f.FolID != \"\" }\nfunc (f *FileOrFolder) ModTime() time.Time {\n\tif f.time == nil {\n\t\ttime := MustParseTime(f.Time)\n\t\tf.time = &time\n\t}\n\treturn *f.time\n}\n\nfunc (f *FileOrFolder) SetShareInfo(fs *FileShare) {\n\tf.shareInfo = fs\n}\nfunc (f *FileOrFolder) GetShareInfo() *FileShare {\n\treturn f.shareInfo\n}\n\n/* 通过ID获取文件/文件夹分享信息 */\ntype FileShare struct {\n\tPwd    string `json:\"pwd\"`\n\tOnof   string `json:\"onof\"`\n\tTaoc   string `json:\"taoc\"`\n\tIsNewd string `json:\"is_newd\"`\n\n\t// 文件\n\tFID string `json:\"f_id\"`\n\n\t// 文件夹\n\tNewUrl string `json:\"new_url\"`\n\tName   string `json:\"name\"`\n\tDes    string `json:\"des\"`\n}\n\n/* 分享类型为文件夹 */\ntype FileOrFolderByShareUrlResp struct {\n\tText []FileOrFolderByShareUrl `json:\"text\"`\n}\ntype FileOrFolderByShareUrl struct {\n\tID      string `json:\"id\"`\n\tNameAll string `json:\"name_all\"`\n\n\t// 文件特有\n\tDuan string `json:\"duan\"`\n\tSize string `json:\"size\"`\n\tTime string `json:\"time\"`\n\t//Icon          string `json:\"icon\"`\n\t//PIco int `json:\"p_ico\"`\n\t//T int `json:\"t\"`\n\n\t// 文件夹特有\n\tIsFloder bool `json:\"-\"`\n\n\t//\n\tUrl string `json:\"-\"`\n\tPwd string `json:\"-\"`\n\n\t// 缓存字段\n\tsize       *int64     `json:\"-\"`\n\ttime       *time.Time `json:\"-\"`\n\trepairFlag bool       `json:\"-\"`\n}\n\nfunc (f *FileOrFolderByShareUrl) CreateTime() time.Time {\n\treturn f.ModTime()\n}\n\nfunc (f *FileOrFolderByShareUrl) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *FileOrFolderByShareUrl) GetID() string   { return f.ID }\nfunc (f *FileOrFolderByShareUrl) GetName() string { return f.NameAll }\nfunc (f *FileOrFolderByShareUrl) GetPath() string { return \"\" }\nfunc (f *FileOrFolderByShareUrl) GetSize() int64 {\n\tif f.size == nil {\n\t\tsize := SizeStrToInt64(f.Size)\n\t\tf.size = &size\n\t}\n\treturn *f.size\n}\nfunc (f *FileOrFolderByShareUrl) IsDir() bool { return f.IsFloder }\nfunc (f *FileOrFolderByShareUrl) ModTime() time.Time {\n\tif f.time == nil {\n\t\ttime := MustParseTime(f.Time)\n\t\tf.time = &time\n\t}\n\treturn *f.time\n}\n\n// 获取下载链接的响应\ntype FileShareInfoAndUrlResp[T string | int] struct {\n\tDom string `json:\"dom\"`\n\tURL string `json:\"url\"`\n\tInf T      `json:\"inf\"`\n}\n\nfunc (u *FileShareInfoAndUrlResp[T]) GetBaseUrl() string {\n\treturn fmt.Sprint(u.Dom, \"/file\")\n}\n\nfunc (u *FileShareInfoAndUrlResp[T]) GetDownloadUrl() string {\n\treturn fmt.Sprint(u.GetBaseUrl(), \"/\", u.URL)\n}\n"
  },
  {
    "path": "drivers/lanzou/util.go",
    "content": "package lanzou\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar upClient *resty.Client\nvar once sync.Once\n\nfunc (d *LanZou) doupload(callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treturn d.post(d.BaseUrl+\"/doupload.php\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"uid\": d.uid,\n\t\t\t\"vei\": d.vei,\n\t\t})\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t}, resp)\n}\n\nfunc (d *LanZou) get(url string, callback base.ReqCallback) ([]byte, error) {\n\treturn d.request(url, http.MethodGet, callback, false)\n}\n\nfunc (d *LanZou) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tdata, err := d._post(url, callback, resp, false)\n\tif err == ErrCookieExpiration && d.IsAccount() {\n\t\tif atomic.CompareAndSwapInt32(&d.flag, 0, 1) {\n\t\t\t_, err2 := d.Login()\n\t\t\tatomic.SwapInt32(&d.flag, 0)\n\t\t\tif err2 != nil {\n\t\t\t\terr = errors.Join(err, err2)\n\t\t\t\td.Status = err.Error()\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t\treturn data, err\n\t\t\t}\n\t\t}\n\t\tfor atomic.LoadInt32(&d.flag) != 0 {\n\t\t\truntime.Gosched()\n\t\t}\n\t\treturn d._post(url, callback, resp, false)\n\t}\n\treturn data, err\n}\n\nfunc (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) {\n\tdata, err := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.AddRetryCondition(func(r *resty.Response, err error) bool {\n\t\t\tif utils.Json.Get(r.Body(), \"zt\").ToInt() == 4 {\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t}, up)\n\tif err != nil {\n\t\treturn data, err\n\t}\n\tswitch utils.Json.Get(data, \"zt\").ToInt() {\n\tcase 1, 2, 4:\n\t\tif resp != nil {\n\t\t\t// 返回类型不统一,忽略错误\n\t\t\tutils.Json.Unmarshal(data, resp)\n\t\t}\n\t\treturn data, nil\n\tcase 9: // 登录过期\n\t\treturn data, ErrCookieExpiration\n\tdefault:\n\t\tinfo := utils.Json.Get(data, \"inf\").ToString()\n\t\tif info == \"\" {\n\t\t\tinfo = utils.Json.Get(data, \"info\").ToString()\n\t\t}\n\t\treturn data, fmt.Errorf(info)\n\t}\n}\n\n// 修复点：所有请求都自动处理 acw_sc__v2 验证和 down_ip=1\nfunc (d *LanZou) request(url string, method string, callback base.ReqCallback, up bool) ([]byte, error) {\n\tvar req *resty.Request\n\tvar vs string\n\tfor retry := 0; retry < 3; retry++ {\n\t\tif up {\n\t\t\tonce.Do(func() {\n\t\t\t\tupClient = base.NewRestyClient().SetTimeout(120 * time.Second)\n\t\t\t})\n\t\t\treq = upClient.R()\n\t\t} else {\n\t\t\treq = base.RestyClient.R()\n\t\t}\n\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Referer\":    \"https://pc.woozooo.com\",\n\t\t\t\"User-Agent\": d.UserAgent,\n\t\t})\n\n\t\t// 下载直链时需要加 down_ip=1\n\t\tif strings.Contains(url, \"/file/\") {\n\t\t\tcookie := d.Cookie\n\t\t\tif cookie != \"\" {\n\t\t\t\tcookie += \"; \"\n\t\t\t}\n\t\t\tcookie += \"down_ip=1\"\n\t\t\tif vs != \"\" {\n\t\t\t\tcookie += \"; acw_sc__v2=\" + vs\n\t\t\t}\n\t\t\treq.SetHeader(\"cookie\", cookie)\n\t\t} else if d.Cookie != \"\" {\n\t\t\tcookie := d.Cookie\n\t\t\tif vs != \"\" {\n\t\t\t\tcookie += \"; acw_sc__v2=\" + vs\n\t\t\t}\n\t\t\treq.SetHeader(\"cookie\", cookie)\n\t\t} else if vs != \"\" {\n\t\t\treq.SetHeader(\"cookie\", \"acw_sc__v2=\"+vs)\n\t\t}\n\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\n\t\tres, err := req.Execute(method, url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbodyStr := res.String()\n\t\tlog.Debugf(\"lanzou request: url=>%s ,stats=>%d ,body => %s\\n\", res.Request.URL, res.StatusCode(), bodyStr)\n\t\tif strings.Contains(bodyStr, \"acw_sc__v2\") {\n\t\t\tvs, err = CalcAcwScV2(bodyStr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\treturn res.Body(), err\n\t}\n\treturn nil, errors.New(\"acw_sc__v2 validation error\")\n}\n\nfunc (d *LanZou) Login() ([]*http.Cookie, error) {\n\tresp, err := base.NewRestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()).\n\t\tR().SetFormData(map[string]string{\n\t\t\"task\":         \"3\",\n\t\t\"uid\":          d.Account,\n\t\t\"pwd\":          d.Password,\n\t\t\"setSessionId\": \"\",\n\t\t\"setSig\":       \"\",\n\t\t\"setScene\":     \"\",\n\t\t\"setTocen\":     \"\",\n\t\t\"formhash\":     \"\",\n\t}).Post(\"https://up.woozooo.com/mlogin.php\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif utils.Json.Get(resp.Body(), \"zt\").ToInt() != 1 {\n\t\treturn nil, fmt.Errorf(\"login err: %s\", resp.Body())\n\t}\n\td.Cookie = CookieToString(resp.Cookies())\n\treturn resp.Cookies(), nil\n}\n\n/*\n通过cookie获取数据\n*/\n\n// 获取文件和文件夹,获取到的文件大小、更改时间不可信\nfunc (d *LanZou) GetAllFiles(folderID string) ([]model.Obj, error) {\n\tfolders, err := d.GetFolders(folderID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := d.GetFiles(folderID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn append(\n\t\tutils.MustSliceConvert(folders, func(folder FileOrFolder) model.Obj {\n\t\t\treturn &folder\n\t\t}), utils.MustSliceConvert(files, func(file FileOrFolder) model.Obj {\n\t\t\treturn &file\n\t\t})...,\n\t), nil\n}\n\n// 通过ID获取文件夹\nfunc (d *LanZou) GetFolders(folderID string) ([]FileOrFolder, error) {\n\tvar resp RespText[[]FileOrFolder]\n\t_, err := d.doupload(func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"task\":      \"47\",\n\t\t\t\"folder_id\": folderID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Text, nil\n}\n\n// 通过ID获取文件\nfunc (d *LanZou) GetFiles(folderID string) ([]FileOrFolder, error) {\n\tfiles := make([]FileOrFolder, 0)\n\tfor pg := 1; ; pg++ {\n\t\tvar resp RespText[[]FileOrFolder]\n\t\t_, err := d.doupload(func(req *resty.Request) {\n\t\t\treq.SetFormData(map[string]string{\n\t\t\t\t\"task\":      \"5\",\n\t\t\t\t\"folder_id\": folderID,\n\t\t\t\t\"pg\":        strconv.Itoa(pg),\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(resp.Text) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfiles = append(files, resp.Text...)\n\t}\n\treturn files, nil\n}\n\n// 通过ID获取文件夹分享地址\nfunc (d *LanZou) getFolderShareUrlByID(fileID string) (*FileShare, error) {\n\tvar resp RespInfo[FileShare]\n\t_, err := d.doupload(func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"task\":    \"18\",\n\t\t\t\"file_id\": fileID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.Info, nil\n}\n\n// 通过ID获取文件分享地址\nfunc (d *LanZou) getFileShareUrlByID(fileID string) (*FileShare, error) {\n\tvar resp RespInfo[FileShare]\n\t_, err := d.doupload(func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"task\":    \"22\",\n\t\t\t\"file_id\": fileID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.Info, nil\n}\n\n/*\n通过分享链接获取数据\n*/\n\n// 判断类容\nvar isFileReg = regexp.MustCompile(`class=\"fileinfo\"|id=\"file\"|文件描述`)\nvar isFolderReg = regexp.MustCompile(`id=\"infos\"`)\n\n// 获取文件文件夹基础信息\n\n// 获取文件名称\nvar nameFindReg = regexp.MustCompile(`<title>(.+?) - 蓝奏云</title>|id=\"filenajax\">(.+?)</div>|var filename = '(.+?)';|<div style=\"font-size.+?>([^<>].+?)</div>|<div class=\"filethetext\".+?>([^<>]+?)</div>`)\n\n// 获取文件大小\nvar sizeFindReg = regexp.MustCompile(`(?i)大小\\W*([0-9.]+\\s*[bkm]+)`)\n\n// 获取文件时间\nvar timeFindReg = regexp.MustCompile(`\\d+\\s*[秒天分小][钟时]?前|[昨前]天|\\d{4}-\\d{2}-\\d{2}`)\n\n// 查找分享文件夹子文件夹ID和名称\nvar findSubFolderReg = regexp.MustCompile(`(?i)(?:folderlink|mbxfolder).+href=\"/(.+?)\"(?:.+filename\")?>(.+?)<`)\n\n// 获取下载页面链接\nvar findDownPageParamReg = regexp.MustCompile(`<iframe.*?src=\"(.+?)\"`)\n\n// 获取文件ID\nvar findFileIDReg = regexp.MustCompile(`'/ajaxm\\.php\\?file=(\\d+)'`)\n\n// 获取分享链接主界面\nfunc (d *LanZou) getShareUrlHtml(shareID string) (string, error) {\n\tvar vs string\n\tfor i := 0; i < 3; i++ {\n\t\tfirstPageData, err := d.get(fmt.Sprint(d.ShareUrl, \"/\", shareID),\n\t\t\tfunc(req *resty.Request) {\n\t\t\t\tif vs != \"\" {\n\t\t\t\t\treq.SetCookie(&http.Cookie{\n\t\t\t\t\t\tName:  \"acw_sc__v2\",\n\t\t\t\t\t\tValue: vs,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfirstPageDataStr := RemoveNotes(string(firstPageData))\n\t\tif strings.Contains(firstPageDataStr, \"取消分享\") {\n\t\t\treturn \"\", ErrFileShareCancel\n\t\t}\n\t\tif strings.Contains(firstPageDataStr, \"文件不存在\") {\n\t\t\treturn \"\", ErrFileNotExist\n\t\t}\n\n\t\t// acw_sc__v2\n\t\tif strings.Contains(firstPageDataStr, \"acw_sc__v2\") {\n\t\t\tif vs, err = CalcAcwScV2(firstPageDataStr); err != nil {\n\t\t\t\tlog.Errorf(\"lanzou: err => acw_sc__v2 validation error  ,data => %s\\n\", firstPageDataStr)\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\treturn firstPageDataStr, nil\n\t}\n\treturn \"\", errors.New(\"acw_sc__v2 validation error\")\n}\n\n// 通过分享链接获取文件或文件夹\nfunc (d *LanZou) GetFileOrFolderByShareUrl(shareID, pwd string) ([]model.Obj, error) {\n\tpageData, err := d.getShareUrlHtml(shareID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !isFileReg.MatchString(pageData) {\n\t\tfiles, err := d.getFolderByShareUrl(pwd, pageData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn utils.MustSliceConvert(files, func(file FileOrFolderByShareUrl) model.Obj {\n\t\t\treturn &file\n\t\t}), nil\n\t} else {\n\t\tfile, err := d.getFilesByShareUrl(shareID, pwd, pageData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []model.Obj{file}, nil\n\t}\n}\n\n// 通过分享链接获取文件(下载链接也使用此方法)\n// FileOrFolderByShareUrl 包含 pwd 和 url 字段\n// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L440\nfunc (d *LanZou) GetFilesByShareUrl(shareID, pwd string) (file *FileOrFolderByShareUrl, err error) {\n\tpageData, err := d.getShareUrlHtml(shareID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.getFilesByShareUrl(shareID, pwd, pageData)\n}\n\nfunc (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (*FileOrFolderByShareUrl, error) {\n\tvar (\n\t\tparam       map[string]string\n\t\tdownloadUrl string\n\t\tbaseUrl     string\n\t\tfile        FileOrFolderByShareUrl\n\t)\n\n\t// 删除注释\n\tsharePageData = RemoveNotes(sharePageData)\n\tsharePageData = RemoveJSComment(sharePageData)\n\n\t// 需要密码\n\tif strings.Contains(sharePageData, \"pwdload\") || strings.Contains(sharePageData, \"passwddiv\") {\n\t\tsharePageData, err := getJSFunctionByName(sharePageData, \"down_p\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparam, err := htmlJsonToMap(sharePageData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparam[\"p\"] = pwd\n\n\t\tfileIDs := findFileIDReg.FindStringSubmatch(sharePageData)\n\t\tvar fileID string\n\t\tif len(fileIDs) > 1 {\n\t\t\tfileID = fileIDs[1]\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"not find file id\")\n\t\t}\n\t\tvar resp FileShareInfoAndUrlResp[string]\n\t\t_, err = d.post(d.ShareUrl+\"/ajaxm.php?file=\"+fileID, func(req *resty.Request) { req.SetFormData(param) }, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfile.NameAll = resp.Inf\n\t\tfile.Pwd = pwd\n\t\tbaseUrl = resp.GetBaseUrl()\n\t\tdownloadUrl = resp.GetDownloadUrl()\n\t} else {\n\t\turlpaths := findDownPageParamReg.FindStringSubmatch(sharePageData)\n\t\tif len(urlpaths) != 2 {\n\t\t\tlog.Errorf(\"lanzou: err => not find file page param ,data => %s\\n\", sharePageData)\n\t\t\treturn nil, fmt.Errorf(\"not find file page param\")\n\t\t}\n\t\tdata, err := d.get(fmt.Sprint(d.ShareUrl, urlpaths[1]), nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnextPageData := RemoveNotes(string(data))\n\t\tparam, err = htmlJsonToMap(nextPageData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfileIDs := findFileIDReg.FindStringSubmatch(nextPageData)\n\t\tvar fileID string\n\t\tif len(fileIDs) > 1 {\n\t\t\tfileID = fileIDs[1]\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"not find file id\")\n\t\t}\n\t\tvar resp FileShareInfoAndUrlResp[int]\n\t\t_, err = d.post(d.ShareUrl+\"/ajaxm.php?file=\"+fileID, func(req *resty.Request) { req.SetFormData(param) }, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbaseUrl = resp.GetBaseUrl()\n\t\tdownloadUrl = resp.GetDownloadUrl()\n\n\t\tnames := nameFindReg.FindStringSubmatch(sharePageData)\n\t\tif len(names) > 1 {\n\t\t\tfor _, name := range names[1:] {\n\t\t\t\tif name != \"\" {\n\t\t\t\t\tfile.NameAll = name\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tsizes := sizeFindReg.FindStringSubmatch(sharePageData)\n\tif len(sizes) == 2 {\n\t\tfile.Size = sizes[1]\n\t}\n\tfile.ID = shareID\n\tfile.Time = timeFindReg.FindString(sharePageData)\n\n\t// 重定向获取真实链接\n\tvar (\n\t\tres *resty.Response\n\t\terr error\n\t)\n\tvar vs string\n\tvar bodyStr string\n\tfor i := 0; i < 3; i++ {\n\t\tres, err = base.NoRedirectClient.R().SetHeaders(map[string]string{\n\t\t\t\"accept-language\": \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\",\n\t\t\t\"Referer\":         baseUrl,\n\t\t}).SetDoNotParseResponse(true).\n\t\t\tSetCookie(&http.Cookie{\n\t\t\t\tName:  \"acw_sc__v2\",\n\t\t\t\tValue: vs,\n\t\t\t}).SetHeader(\"cookie\", \"down_ip=1\").Get(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif res.StatusCode() == 302 {\n\t\t\tif res.RawBody() != nil {\n\t\t\t\tres.RawBody().Close()\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tbodyBytes, err := io.ReadAll(res.RawBody())\n\t\tif res.RawBody() != nil {\n\t\t\tres.RawBody().Close()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"读取响应体失败: %w\", err)\n\t\t}\n\t\tbodyStr = string(bodyBytes)\n\t\tif strings.Contains(bodyStr, \"acw_sc__v2\") {\n\t\t\tif vs, err = CalcAcwScV2(bodyStr); err != nil {\n\t\t\t\tlog.Errorf(\"lanzou: err => acw_sc__v2 validation error  ,data => %s\\n\", bodyStr)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfile.Url = res.Header().Get(\"location\")\n\n\t// 触发二次验证，也需要处理一下触发acw_sc__v2的情况\n\tif res.StatusCode() != 302 {\n\t\tparam, err = htmlJsonToMap(bodyStr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparam[\"el\"] = \"2\"\n\t\ttime.Sleep(time.Second * 2)\n\n\t\t// 通过验证获取直链\n\t\tvar data []byte\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tdata, err = d.post(fmt.Sprint(baseUrl, \"/ajax.php\"), func(req *resty.Request) {\n\t\t\t\treq.SetFormData(param)\n\t\t\t\treq.SetHeader(\"cookie\", \"down_ip=1\")\n\t\t\t\tif vs != \"\" {\n\t\t\t\t\treq.SetCookie(&http.Cookie{\n\t\t\t\t\t\tName:  \"acw_sc__v2\",\n\t\t\t\t\t\tValue: vs,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tajaxBodyStr := string(data)\n\t\t\tif strings.Contains(ajaxBodyStr, \"acw_sc__v2\") {\n\t\t\t\tif vs, err = CalcAcwScV2(ajaxBodyStr); err != nil {\n\t\t\t\t\tlog.Errorf(\"lanzou: err => acw_sc__v2 validation error  ,data => %s\\n\", ajaxBodyStr)\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Second * 2)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfile.Url = utils.Json.Get(data, \"url\").ToString()\n\t}\n\treturn &file, nil\n}\n\n// 通过分享链接获取文件夹\n// 似乎子目录和文件不会加密\n// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L1089\nfunc (d *LanZou) GetFolderByShareUrl(shareID, pwd string) ([]FileOrFolderByShareUrl, error) {\n\tpageData, err := d.getShareUrlHtml(shareID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.getFolderByShareUrl(pwd, pageData)\n}\n\nfunc (d *LanZou) getFolderByShareUrl(pwd string, sharePageData string) ([]FileOrFolderByShareUrl, error) {\n\tfrom, err := htmlJsonToMap(sharePageData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfiles := make([]FileOrFolderByShareUrl, 0)\n\t// vip获取文件夹\n\tfloders := findSubFolderReg.FindAllStringSubmatch(sharePageData, -1)\n\tfor _, floder := range floders {\n\t\tif len(floder) == 3 {\n\t\t\tfiles = append(files, FileOrFolderByShareUrl{\n\t\t\t\t// Pwd: pwd, // 子文件夹不加密\n\t\t\t\tID:       floder[1],\n\t\t\t\tNameAll:  floder[2],\n\t\t\t\tIsFloder: true,\n\t\t\t})\n\t\t}\n\t}\n\n\t// 获取文件\n\tfrom[\"pwd\"] = pwd\n\tfor page := 1; ; page++ {\n\t\tfrom[\"pg\"] = strconv.Itoa(page)\n\t\tvar resp FileOrFolderByShareUrlResp\n\t\t_, err := d.post(d.ShareUrl+\"/filemoreajax.php\", func(req *resty.Request) { req.SetFormData(from) }, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 文件夹中的文件加密\n\t\tfor i := 0; i < len(resp.Text); i++ {\n\t\t\tresp.Text[i].Pwd = pwd\n\t\t}\n\t\tif len(resp.Text) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfiles = append(files, resp.Text...)\n\t\ttime.Sleep(time.Second)\n\t}\n\treturn files, nil\n}\n\n// 通过下载头获取真实文件信息\nfunc (d *LanZou) getFileRealInfo(downURL string) (*int64, *time.Time) {\n\tres, _ := base.RestyClient.R().Head(downURL)\n\tif res == nil {\n\t\treturn nil, nil\n\t}\n\ttime, _ := http.ParseTime(res.Header().Get(\"Last-Modified\"))\n\tsize, _ := strconv.ParseInt(res.Header().Get(\"Content-Length\"), 10, 64)\n\treturn &size, &time\n}\n\nfunc (d *LanZou) getVeiAndUid() (vei string, uid string, err error) {\n\tvar resp []byte\n\tresp, err = d.get(\"https://pc.woozooo.com/mydisk.php\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"item\":   \"files\",\n\t\t\t\"action\": \"index\",\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\t// uid\n\tuids := regexp.MustCompile(`uid=([^'\"&;]+)`).FindStringSubmatch(string(resp))\n\tif len(uids) < 2 {\n\t\terr = fmt.Errorf(\"uid variable not find\")\n\t\treturn\n\t}\n\tuid = uids[1]\n\n\t// vei\n\thtml := RemoveNotes(string(resp))\n\tdata, err := htmlJsonToMap(html)\n\tif err != nil {\n\t\treturn\n\t}\n\tvei = data[\"vei\"]\n\n\treturn\n}\n"
  },
  {
    "path": "drivers/lenovonas_share/driver.go",
    "content": "package LenovoNasShare\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype LenovoNasShare struct {\n\tmodel.Storage\n\tAddition\n\tstoken   string\n\texpireAt int64\n}\n\nfunc (d *LenovoNasShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *LenovoNasShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *LenovoNasShare) Init(ctx context.Context) error {\n\tif err := d.getStoken(); err != nil {\n\t\treturn err\n\t}\n\tif !d.ShowRootFolder && d.RootFolderPath == \"\" {\n\t\tlist, _ := d.List(ctx, File{}, model.ListArgs{})\n\t\td.RootFolderPath = list[0].GetPath()\n\t}\n\treturn nil\n}\n\nfunc (d *LenovoNasShare) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *LenovoNasShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\td.checkStoken() // 检查stoken是否过期\n\tpath := fmt.Sprintf(\"/%s\", strings.Trim(dir.GetPath(), \"/\"))\n\n\tvar resp Files\n\tquery := map[string]string{\n\t\t\"code\":   d.ShareId,\n\t\t\"num\":    \"5000\",\n\t\t\"stoken\": d.stoken,\n\t\t\"path\":   path,\n\t}\n\t_, err := d.request(d.Host+\"/oneproxy/api/share/v1/files\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.SliceConvert(resp.Data.List, func(src File) (model.Obj, error) {\n\t\tif src.IsDir() {\n\t\t\treturn src, nil\n\t\t}\n\t\treturn &model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     src.GetName(),\n\t\t\t\tPath:     src.GetPath(),\n\t\t\t\tSize:     src.GetSize(),\n\t\t\t\tModified: src.ModTime(),\n\t\t\t\tIsFolder: src.IsDir(),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\tThumbnail: func() string {\n\t\t\t\t\tthumbUrl := d.Host + \"/oneproxy/api/share/v1/file/thumb?code=\" + d.ShareId + \"&stoken=\" + d.stoken + \"&path=\" + url.QueryEscape(src.GetPath())\n\t\t\t\t\treturn thumbUrl\n\t\t\t\t}(),\n\t\t\t},\n\t\t}, nil\n\t})\n}\n\nfunc (d *LenovoNasShare) checkStoken() { // 检查stoken是否过期\n\tif d.expireAt < time.Now().Unix() {\n\t\td.getStoken()\n\t}\n}\n\nfunc (d *LenovoNasShare) getStoken() error { // 获取stoken\n\tif d.Host == \"\" {\n\t\td.Host = \"https://siot-share.lenovo.com.cn\"\n\t}\n\n\tparts := strings.Split(d.ShareId, \"/\")\n\td.ShareId = parts[len(parts)-1]\n\n\tquery := map[string]string{\n\t\t\"code\":     d.ShareId,\n\t\t\"password\": d.SharePwd,\n\t}\n\tresp, err := d.request(d.Host+\"/oneproxy/api/share/v1/access\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.stoken = utils.Json.Get(resp, \"data\", \"stoken\").ToString()\n\td.expireAt = utils.Json.Get(resp, \"data\", \"expires_in\").ToInt64() + time.Now().Unix() - 60\n\treturn nil\n}\n\nfunc (d *LenovoNasShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\td.checkStoken() // 检查stoken是否过期\n\tquery := map[string]string{\n\t\t\"code\":   d.ShareId,\n\t\t\"stoken\": d.stoken,\n\t\t\"path\":   file.GetPath(),\n\t}\n\tresp, err := d.request(d.Host+\"/oneproxy/api/share/v1/file/link\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdownloadUrl := d.Host + \"/oneproxy/api/share/v1/file/download?code=\" + d.ShareId + \"&dtoken=\" + utils.Json.Get(resp, \"data\", \"param\", \"dtoken\").ToString()\n\n\tlink := model.Link{\n\t\tURL: downloadUrl,\n\t\tHeader: http.Header{\n\t\t\t\"Referer\": []string{\"https://siot-share.lenovo.com.cn\"},\n\t\t},\n\t}\n\treturn &link, nil\n}\n\nfunc (d *LenovoNasShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *LenovoNasShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *LenovoNasShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *LenovoNasShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *LenovoNasShare) Remove(ctx context.Context, obj model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *LenovoNasShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nvar _ driver.Driver = (*LenovoNasShare)(nil)\n"
  },
  {
    "path": "drivers/lenovonas_share/meta.go",
    "content": "package LenovoNasShare\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tShareId        string `json:\"share_id\" required:\"true\" help:\"The part after the last / in the shared link\"`\n\tSharePwd       string `json:\"share_pwd\" required:\"true\" help:\"The password of the shared link\"`\n\tHost           string `json:\"host\" required:\"true\" default:\"https://siot-share.lenovo.com.cn\" help:\"You can change it to your local area network\"`\n\tShowRootFolder bool   `json:\"show_root_folder\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:      \"LenovoNasShare\",\n\tLocalSort: true,\n\tNoUpload:  true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &LenovoNasShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/lenovonas_share/types.go",
    "content": "package LenovoNasShare\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nfunc (f *File) UnmarshalJSON(data []byte) error {\n\ttype Alias File\n\taux := &struct {\n\t\tCreateAt int64 `json:\"time\"`\n\t\tUpdateAt int64 `json:\"chtime\"`\n\t\t*Alias\n\t}{\n\t\tAlias: (*Alias)(f),\n\t}\n\n\tif err := json.Unmarshal(data, aux); err != nil {\n\t\treturn err\n\t}\n\n\tf.CreateAt = time.Unix(aux.CreateAt, 0)\n\tf.UpdateAt = time.Unix(aux.UpdateAt, 0)\n\n\treturn nil\n}\n\ntype File struct {\n\tFileName string    `json:\"name\"`\n\tSize     int64     `json:\"size\"`\n\tCreateAt time.Time `json:\"time\"`\n\tUpdateAt time.Time `json:\"chtime\"`\n\tPath     string    `json:\"path\"`\n\tType     string    `json:\"type\"`\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f File) GetPath() string {\n\treturn f.Path\n}\n\nfunc (f File) GetSize() int64 {\n\tif f.IsDir() {\n\t\treturn 0\n\t} else {\n\t\treturn f.Size\n\t}\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) ModTime() time.Time {\n\treturn f.UpdateAt\n}\n\nfunc (f File) CreateTime() time.Time {\n\treturn f.CreateAt\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == \"dir\"\n}\n\nfunc (f File) GetID() string {\n\treturn f.GetPath()\n}\n\ntype Files struct {\n\tData struct {\n\t\tList    []File `json:\"list\"`\n\t\tHasMore bool   `json:\"has_more\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/lenovonas_share/util.go",
    "content": "package LenovoNasShare\n\nimport (\n\t\"errors\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nfunc (d *LenovoNasShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"origin\":      \"https://siot-share.lenovo.com.cn\",\n\t\t\"referer\":     \"https://siot-share.lenovo.com.cn/\",\n\t\t\"user-agent\":  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client\",\n\t\t\"platform\":    \"web\",\n\t\t\"app-version\": \"3\",\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbody := res.Body()\n\tresult := utils.Json.Get(body, \"result\").ToBool()\n\tif !result {\n\t\treturn nil, errors.New(jsoniter.Get(body, \"error\", \"msg\").ToString())\n\t}\n\treturn body, nil\n}\n"
  },
  {
    "path": "drivers/local/benchmark_calculatedirsize_test.go",
    "content": "package local\n\n// TestDirCalculateSize tests the directory size calculation\n// It should be run with the local driver enabled and directory size calculation set to true\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n)\n\nfunc generatedTestDir(dir string, dep, filecount int) {\n\tif dep == 0 {\n\t\treturn\n\t}\n\tfor i := 0; i < dep; i++ {\n\t\tsubDir := dir + \"/dir\" + strconv.Itoa(i)\n\t\tos.Mkdir(subDir, 0755)\n\t\tgeneratedTestDir(subDir, dep-1, filecount)\n\t\tgeneratedFiles(subDir, filecount)\n\t}\n}\n\nfunc generatedFiles(path string, count int) error {\n\tfor i := 0; i < count; i++ {\n\t\tfilePath := filepath.Join(path, \"file\"+strconv.Itoa(i)+\".txt\")\n\t\tfile, err := os.Create(filePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 使用随机ascii字符填充文件\n\t\tcontent := make([]byte, 1024) // 1KB file\n\t\tfor j := range content {\n\t\t\tcontent[j] = byte('a' + j%26) // Fill with 'a' to 'z'\n\t\t}\n\t\t_, err = file.Write(content)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfile.Close()\n\t}\n\treturn nil\n}\n\n// performance tests for directory size calculation\nfunc BenchmarkCalculateDirSize(t *testing.B) {\n\t// 初始化t的日志\n\tt.Logf(\"Starting performance test for directory size calculation\")\n\t// 确保测试目录存在\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping performance test in short mode\")\n\t}\n\t// 创建tmp directory for testing\n\ttestTempDir := t.TempDir()\n\terr := os.MkdirAll(testTempDir, 0755)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create test directory: %v\", err)\n\t}\n\tdefer os.RemoveAll(testTempDir) // Clean up after test\n\t// 构建一个深度为5，每层10个文件和10个目录的目录结构\n\tgeneratedTestDir(testTempDir, 5, 10)\n\t// Initialize the local driver with directory size calculation enabled\n\td := &Local{\n\t\tdirectoryMap: DirectoryMap{\n\t\t\troot: testTempDir,\n\t\t},\n\t\tAddition: Addition{\n\t\t\tDirectorySize: true,\n\t\t\tRootPath: driver.RootPath{\n\t\t\t\tRootFolderPath: testTempDir,\n\t\t\t},\n\t\t},\n\t}\n\t//record the start time\n\tt.StartTimer()\n\t// Calculate the directory size\n\terr = d.directoryMap.RecalculateDirSize()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to calculate directory size: %v\", err)\n\t}\n\t//record the end time\n\tt.StopTimer()\n\t// Print the size and duration\n\tnode, ok := d.directoryMap.Get(d.directoryMap.root)\n\tif !ok {\n\t\tt.Fatalf(\"Failed to get root node from directory map\")\n\t}\n\tt.Logf(\"Directory size: %d bytes\", node.fileSum+node.directorySum)\n\tt.Logf(\"Performance test completed successfully\")\n}\n"
  },
  {
    "path": "drivers/local/copy_namedpipes.go",
    "content": "//go:build !windows && !plan9 && !netbsd && !aix && !illumos && !solaris && !js\n\npackage local\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"syscall\"\n)\n\nfunc copyNamedPipe(dstPath string, mode os.FileMode, dirMode os.FileMode) error {\n\tif err := os.MkdirAll(filepath.Dir(dstPath), dirMode); err != nil {\n\t\treturn err\n\t}\n\treturn syscall.Mkfifo(dstPath, uint32(mode))\n}\n"
  },
  {
    "path": "drivers/local/copy_namedpipes_x.go",
    "content": "//go:build windows || plan9 || netbsd || aix || illumos || solaris || js\n\npackage local\n\nimport \"os\"\n\nfunc copyNamedPipe(_ string, _, _ os.FileMode) error {\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/local/driver.go",
    "content": "package local\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/times\"\n\tlog \"github.com/sirupsen/logrus\"\n\t_ \"golang.org/x/image/webp\"\n)\n\ntype Local struct {\n\tmodel.Storage\n\tAddition\n\tmkdirPerm int32\n\n\t// directory size data\n\tdirectoryMap DirectoryMap\n\n\t// zero means no limit\n\tthumbConcurrency int\n\tthumbTokenBucket TokenBucket\n\n\t// video thumb position\n\tvideoThumbPos             float64\n\tvideoThumbPosIsPercentage bool\n}\n\nfunc (d *Local) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Local) Init(ctx context.Context) error {\n\tif d.MkdirPerm == \"\" {\n\t\td.mkdirPerm = 0o777\n\t} else {\n\t\tv, err := strconv.ParseUint(d.MkdirPerm, 8, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.mkdirPerm = int32(v)\n\t}\n\tif !utils.Exists(d.GetRootPath()) {\n\t\treturn fmt.Errorf(\"root folder %s not exists\", d.GetRootPath())\n\t}\n\tif !filepath.IsAbs(d.GetRootPath()) {\n\t\tabs, err := filepath.Abs(d.GetRootPath())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.Addition.RootFolderPath = abs\n\t}\n\tif d.DirectorySize {\n\t\td.directoryMap.root = d.GetRootPath()\n\t\t_, err := d.directoryMap.CalculateDirSize(d.GetRootPath())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\td.directoryMap.Clear()\n\t}\n\tif d.ThumbCacheFolder != \"\" && !utils.Exists(d.ThumbCacheFolder) {\n\t\terr := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif d.ThumbConcurrency != \"\" {\n\t\tv, err := strconv.ParseUint(d.ThumbConcurrency, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.thumbConcurrency = int(v)\n\t}\n\tif d.thumbConcurrency == 0 {\n\t\td.thumbTokenBucket = NewNopTokenBucket()\n\t} else {\n\t\td.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)\n\t}\n\t// Check the VideoThumbPos value\n\tif d.VideoThumbPos == \"\" {\n\t\td.VideoThumbPos = \"20%\"\n\t}\n\tif strings.HasSuffix(d.VideoThumbPos, \"%\") {\n\t\tpercentage := strings.TrimSuffix(d.VideoThumbPos, \"%\")\n\t\tval, err := strconv.ParseFloat(percentage, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid video_thumb_pos value: %s, err: %s\", d.VideoThumbPos, err)\n\t\t}\n\t\tif val < 0 || val > 100 {\n\t\t\treturn fmt.Errorf(\"invalid video_thumb_pos value: %s, the precentage must be a number between 0 and 100\", d.VideoThumbPos)\n\t\t}\n\t\td.videoThumbPosIsPercentage = true\n\t\td.videoThumbPos = val / 100\n\t} else {\n\t\tval, err := strconv.ParseFloat(d.VideoThumbPos, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid video_thumb_pos value: %s, err: %s\", d.VideoThumbPos, err)\n\t\t}\n\t\tif val < 0 {\n\t\t\treturn fmt.Errorf(\"invalid video_thumb_pos value: %s, the time must be a positive number\", d.VideoThumbPos)\n\t\t}\n\t\td.videoThumbPosIsPercentage = false\n\t\td.videoThumbPos = val\n\t}\n\treturn nil\n}\n\nfunc (d *Local) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Local) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfullPath := dir.GetPath()\n\trawFiles, err := readDir(fullPath)\n\tif d.DirectorySize && args.Refresh {\n\t\td.directoryMap.RecalculateDirSize()\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range rawFiles {\n\t\tif d.ShowHidden || !isHidden(f, fullPath) {\n\t\t\tfiles = append(files, d.FileInfoToObj(ctx, f, args.ReqPath, fullPath))\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (d *Local) FileInfoToObj(ctx context.Context, f fs.FileInfo, reqPath string, fullPath string) model.Obj {\n\tthumb := \"\"\n\tif d.Thumbnail {\n\t\ttypeName := utils.GetFileType(f.Name())\n\t\tif typeName == conf.IMAGE || typeName == conf.VIDEO {\n\t\t\tthumb = common.GetApiUrl(ctx) + stdpath.Join(\"/d\", reqPath, f.Name())\n\t\t\tthumb = utils.EncodePath(thumb, true)\n\t\t\tthumb += \"?type=thumb&sign=\" + sign.Sign(stdpath.Join(reqPath, f.Name()))\n\t\t}\n\t}\n\tisFolder := f.IsDir() || isSymlinkDir(f, fullPath)\n\tvar size int64\n\tif isFolder {\n\t\tnode, ok := d.directoryMap.Get(filepath.Join(fullPath, f.Name()))\n\t\tif ok {\n\t\t\tsize = node.fileSum + node.directorySum\n\t\t}\n\t} else {\n\t\tsize = f.Size()\n\t}\n\tvar ctime time.Time\n\tt, err := times.Stat(stdpath.Join(fullPath, f.Name()))\n\tif err == nil {\n\t\tif t.HasBirthTime() {\n\t\t\tctime = t.BirthTime()\n\t\t}\n\t}\n\n\tfile := model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tPath:     filepath.Join(fullPath, f.Name()),\n\t\t\tName:     f.Name(),\n\t\t\tModified: f.ModTime(),\n\t\t\tSize:     size,\n\t\t\tIsFolder: isFolder,\n\t\t\tCtime:    ctime,\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: thumb,\n\t\t},\n\t}\n\treturn &file\n}\n\nfunc (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {\n\tpath = filepath.Join(d.GetRootPath(), path)\n\tf, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\tisFolder := f.IsDir() || isSymlinkDir(f, path)\n\tsize := f.Size()\n\tif isFolder {\n\t\tnode, ok := d.directoryMap.Get(path)\n\t\tif ok {\n\t\t\tsize = node.fileSum + node.directorySum\n\t\t}\n\t} else {\n\t\tsize = f.Size()\n\t}\n\tvar ctime time.Time\n\tt, err := times.Stat(path)\n\tif err == nil {\n\t\tif t.HasBirthTime() {\n\t\t\tctime = t.BirthTime()\n\t\t}\n\t}\n\tfile := model.Object{\n\t\tPath:     path,\n\t\tName:     f.Name(),\n\t\tModified: f.ModTime(),\n\t\tCtime:    ctime,\n\t\tSize:     size,\n\t\tIsFolder: isFolder,\n\t}\n\treturn &file, nil\n}\n\nfunc (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tfullPath := file.GetPath()\n\tlink := &model.Link{}\n\tvar MFile model.File\n\tif args.Type == \"thumb\" && utils.Ext(file.GetName()) != \"svg\" {\n\t\tvar buf *bytes.Buffer\n\t\tvar thumbPath *string\n\t\terr := d.thumbTokenBucket.Do(ctx, func() error {\n\t\t\tvar err error\n\t\t\tbuf, thumbPath, err = d.getThumb(file)\n\t\t\treturn err\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlink.Header = http.Header{\n\t\t\t\"Content-Type\": []string{\"image/png\"},\n\t\t}\n\t\tif thumbPath != nil {\n\t\t\topen, err := os.Open(*thumbPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Get thumbnail file size for Content-Length\n\t\t\tstat, err := open.Stat()\n\t\t\tif err != nil {\n\t\t\t\topen.Close()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tlink.ContentLength = int64(stat.Size())\n\t\t\tMFile = open\n\t\t} else {\n\t\t\tMFile = bytes.NewReader(buf.Bytes())\n\t\t\tlink.ContentLength = int64(buf.Len())\n\t\t}\n\t} else {\n\t\topen, err := os.Open(fullPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlink.ContentLength = file.GetSize()\n\t\tMFile = open\n\t}\n\tlink.SyncClosers.AddIfCloser(MFile)\n\tlink.RangeReader = stream.GetRangeReaderFromMFile(link.ContentLength, MFile)\n\tlink.RequireReference = link.SyncClosers.Length() > 0\n\treturn link, nil\n}\n\nfunc (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tfullPath := filepath.Join(parentDir.GetPath(), dirName)\n\terr := os.MkdirAll(fullPath, os.FileMode(d.mkdirPerm))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())\n\tif utils.IsSubPath(srcPath, dstPath) {\n\t\treturn fmt.Errorf(\"the destination folder is a subfolder of the source folder\")\n\t}\n\terr := os.Rename(srcPath, dstPath)\n\tif isCrossDeviceError(err) {\n\t\t// 跨设备移动，变更为移动任务\n\t\treturn errs.NotImplement\n\t}\n\tif err == nil {\n\t\tsrcParent := filepath.Dir(srcPath)\n\t\tdstParent := filepath.Dir(dstPath)\n\t\tif d.directoryMap.Has(srcParent) {\n\t\t\td.directoryMap.UpdateDirSize(srcParent)\n\t\t\td.directoryMap.UpdateDirParents(srcParent)\n\t\t}\n\t\tif d.directoryMap.Has(dstParent) {\n\t\t\td.directoryMap.UpdateDirSize(dstParent)\n\t\t\td.directoryMap.UpdateDirParents(dstParent)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(filepath.Dir(srcPath), newName)\n\terr := os.Rename(srcPath, dstPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif srcObj.IsDir() {\n\t\tif d.directoryMap.Has(srcPath) {\n\t\t\td.directoryMap.DeleteDirNode(srcPath)\n\t\t\td.directoryMap.CalculateDirSize(dstPath)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Local) Copy(_ context.Context, srcObj, dstDir model.Obj) error {\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())\n\tif utils.IsSubPath(srcPath, dstPath) {\n\t\treturn fmt.Errorf(\"the destination folder is a subfolder of the source folder\")\n\t}\n\tinfo, err := os.Lstat(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 复制regular文件会返回errs.NotImplement, 转为复制任务\n\tif err = d.tryCopy(srcPath, dstPath, info); err != nil {\n\t\treturn err\n\t}\n\n\tif d.directoryMap.Has(filepath.Dir(dstPath)) {\n\t\td.directoryMap.UpdateDirSize(filepath.Dir(dstPath))\n\t\td.directoryMap.UpdateDirParents(filepath.Dir(dstPath))\n\t}\n\n\treturn nil\n}\n\nfunc (d *Local) Remove(ctx context.Context, obj model.Obj) error {\n\tvar err error\n\tif utils.SliceContains([]string{\"\", \"delete permanently\"}, d.RecycleBinPath) {\n\t\tif obj.IsDir() {\n\t\t\terr = os.RemoveAll(obj.GetPath())\n\t\t} else {\n\t\t\terr = os.Remove(obj.GetPath())\n\t\t}\n\t} else {\n\t\tobjPath := obj.GetPath()\n\t\tobjName := obj.GetName()\n\t\tvar relPath string\n\t\trelPath, err = filepath.Rel(d.GetRootPath(), filepath.Dir(objPath))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trecycleBinPath := filepath.Join(d.RecycleBinPath, relPath)\n\t\tif !utils.Exists(recycleBinPath) {\n\t\t\terr = os.MkdirAll(recycleBinPath, 0o755)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tdstPath := filepath.Join(recycleBinPath, objName)\n\t\tif utils.Exists(dstPath) {\n\t\t\tdstPath = filepath.Join(recycleBinPath, objName+\"_\"+time.Now().Format(\"20060102150405\"))\n\t\t}\n\t\terr = os.Rename(objPath, dstPath)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tif obj.IsDir() {\n\t\tif d.directoryMap.Has(obj.GetPath()) {\n\t\t\td.directoryMap.DeleteDirNode(obj.GetPath())\n\t\t\td.directoryMap.UpdateDirSize(filepath.Dir(obj.GetPath()))\n\t\t\td.directoryMap.UpdateDirParents(filepath.Dir(obj.GetPath()))\n\t\t}\n\t} else {\n\t\tif d.directoryMap.Has(filepath.Dir(obj.GetPath())) {\n\t\t\td.directoryMap.UpdateDirSize(filepath.Dir(obj.GetPath()))\n\t\t\td.directoryMap.UpdateDirParents(filepath.Dir(obj.GetPath()))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tfullPath := filepath.Join(dstDir.GetPath(), stream.GetName())\n\tout, err := os.Create(fullPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = out.Close()\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\t_ = os.Remove(fullPath)\n\t\t}\n\t}()\n\terr = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.Chtimes(fullPath, stream.ModTime(), stream.ModTime())\n\tif err != nil {\n\t\tlog.Errorf(\"[local] failed to change time of %s: %s\", fullPath, err)\n\t}\n\tif d.directoryMap.Has(dstDir.GetPath()) {\n\t\td.directoryMap.UpdateDirSize(dstDir.GetPath())\n\t\td.directoryMap.UpdateDirParents(dstDir.GetPath())\n\t}\n\n\treturn nil\n}\n\nfunc (d *Local) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tdu, err := getDiskUsage(d.RootFolderPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: du,\n\t}, nil\n}\n\nvar _ driver.Driver = (*Local)(nil)\n"
  },
  {
    "path": "drivers/local/meta.go",
    "content": "package local\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tDirectorySize    bool   `json:\"directory_size\" default:\"false\" help:\"This might impact host performance\"`\n\tThumbnail        bool   `json:\"thumbnail\" required:\"true\" help:\"enable thumbnail\"`\n\tThumbCacheFolder string `json:\"thumb_cache_folder\"`\n\tThumbConcurrency string `json:\"thumb_concurrency\" default:\"16\" required:\"false\" help:\"Number of concurrent thumbnail generation goroutines. This controls how many thumbnails can be generated in parallel.\"`\n\tVideoThumbPos    string `json:\"video_thumb_pos\" default:\"20%\" required:\"false\" help:\"The position of the video thumbnail. If the value is a number (integer ot floating point), it represents the time in seconds. If the value ends with '%', it represents the percentage of the video duration.\"`\n\tShowHidden       bool   `json:\"show_hidden\" default:\"true\" required:\"false\" help:\"show hidden directories and files\"`\n\tMkdirPerm        string `json:\"mkdir_perm\" default:\"777\"`\n\tRecycleBinPath   string `json:\"recycle_bin_path\" default:\"delete permanently\" help:\"path to recycle bin, delete permanently if empty or keep 'delete permanently'\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Local\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tNoCache:     true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Local{\n\t\t\tdirectoryMap: DirectoryMap{},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/local/token_bucket.go",
    "content": "package local\n\nimport \"context\"\n\ntype TokenBucket interface {\n\tTake() <-chan struct{}\n\tPut()\n\tDo(context.Context, func() error) error\n}\n\n// StaticTokenBucket is a bucket with a fixed number of tokens,\n// where the retrieval and return of tokens are manually controlled.\n// In the initial state, the bucket is full.\ntype StaticTokenBucket struct {\n\tbucket chan struct{}\n}\n\nfunc NewStaticTokenBucket(size int) StaticTokenBucket {\n\tbucket := make(chan struct{}, size)\n\tfor range size {\n\t\tbucket <- struct{}{}\n\t}\n\treturn StaticTokenBucket{bucket: bucket}\n}\n\nfunc NewStaticTokenBucketWithMigration(oldBucket TokenBucket, size int) StaticTokenBucket {\n\tif oldBucket != nil {\n\t\toldStaticBucket, ok := oldBucket.(StaticTokenBucket)\n\t\tif ok {\n\t\t\toldSize := cap(oldStaticBucket.bucket)\n\t\t\tmigrateSize := oldSize\n\t\t\tif size < migrateSize {\n\t\t\t\tmigrateSize = size\n\t\t\t}\n\n\t\t\tbucket := make(chan struct{}, size)\n\t\t\tfor range size - migrateSize {\n\t\t\t\tbucket <- struct{}{}\n\t\t\t}\n\n\t\t\tif migrateSize != 0 {\n\t\t\t\tgo func() {\n\t\t\t\t\tfor range migrateSize {\n\t\t\t\t\t\t<-oldStaticBucket.bucket\n\t\t\t\t\t\tbucket <- struct{}{}\n\t\t\t\t\t}\n\t\t\t\t\tclose(oldStaticBucket.bucket)\n\t\t\t\t}()\n\t\t\t}\n\t\t\treturn StaticTokenBucket{bucket: bucket}\n\t\t}\n\t}\n\treturn NewStaticTokenBucket(size)\n}\n\n// Take channel maybe closed when local driver is modified.\n// don't call Put method after the channel is closed.\nfunc (b StaticTokenBucket) Take() <-chan struct{} {\n\treturn b.bucket\n}\n\nfunc (b StaticTokenBucket) Put() {\n\tb.bucket <- struct{}{}\n}\n\nfunc (b StaticTokenBucket) Do(ctx context.Context, f func() error) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase _, ok := <-b.Take():\n\t\tif ok {\n\t\t\tdefer b.Put()\n\t\t}\n\t}\n\treturn f()\n}\n\n// NopTokenBucket all function calls to this bucket will success immediately\ntype NopTokenBucket struct {\n\tnop chan struct{}\n}\n\nfunc NewNopTokenBucket() NopTokenBucket {\n\tnop := make(chan struct{})\n\tclose(nop)\n\treturn NopTokenBucket{nop}\n}\n\nfunc (b NopTokenBucket) Take() <-chan struct{} {\n\treturn b.nop\n}\n\nfunc (b NopTokenBucket) Put() {}\n\nfunc (b NopTokenBucket) Do(_ context.Context, f func() error) error { return f() }\n"
  },
  {
    "path": "drivers/local/util.go",
    "content": "package local\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/KarpelesLab/reflink\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/disintegration/imaging\"\n\tffmpeg \"github.com/u2takey/ffmpeg-go\"\n)\n\nfunc isSymlinkDir(f fs.FileInfo, path string) bool {\n\tif f.Mode()&os.ModeSymlink == os.ModeSymlink ||\n\t\t(runtime.GOOS == \"windows\" && f.Mode()&os.ModeIrregular == os.ModeIrregular) { // os.ModeIrregular is Junction bit in Windows\n\t\tdst, err := os.Readlink(filepath.Join(path, f.Name()))\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tif !filepath.IsAbs(dst) {\n\t\t\tdst = filepath.Join(path, dst)\n\t\t}\n\t\tstat, err := os.Stat(dst)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn stat.IsDir()\n\t}\n\treturn false\n}\n\n// Get the snapshot of the video\nfunc (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {\n\t// Run ffprobe to get the video duration\n\tjsonOutput, err := ffmpeg.Probe(videoPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// get format.duration from the json string\n\ttype probeFormat struct {\n\t\tDuration string `json:\"duration\"`\n\t}\n\ttype probeData struct {\n\t\tFormat probeFormat `json:\"format\"`\n\t}\n\tvar probe probeData\n\terr = json.Unmarshal([]byte(jsonOutput), &probe)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotalDuration, err := strconv.ParseFloat(probe.Format.Duration, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ss string\n\tif d.videoThumbPosIsPercentage {\n\t\tss = fmt.Sprintf(\"%f\", totalDuration*d.videoThumbPos)\n\t} else {\n\t\t// If the value is greater than the total duration, use the total duration\n\t\tif d.videoThumbPos > totalDuration {\n\t\t\tss = fmt.Sprintf(\"%f\", totalDuration)\n\t\t} else {\n\t\t\tss = fmt.Sprintf(\"%f\", d.videoThumbPos)\n\t\t}\n\t}\n\n\t// Run ffmpeg to get the snapshot\n\tsrcBuf := bytes.NewBuffer(nil)\n\t// If the remaining time from the seek point to the end of the video is less\n\t// than the duration of a single frame, ffmpeg cannot extract any frames\n\t// within the specified range and will exit with an error.\n\t// The \"noaccurate_seek\" option prevents this error and would also speed up\n\t// the seek process.\n\tstream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{\"ss\": ss, \"noaccurate_seek\": \"\"}).\n\t\tOutput(\"pipe:\", ffmpeg.KwArgs{\"vframes\": 1, \"format\": \"image2\", \"vcodec\": \"mjpeg\"}).\n\t\tGlobalArgs(\"-loglevel\", \"error\").Silent(true).\n\t\tWithOutput(srcBuf, os.Stdout)\n\tif err = stream.Run(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn srcBuf, nil\n}\n\nfunc readDir(dirname string) ([]fs.FileInfo, error) {\n\tf, err := os.Open(dirname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlist, err := f.Readdir(-1)\n\tf.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })\n\treturn list, nil\n}\n\nfunc (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {\n\tfullPath := file.GetPath()\n\tthumbPrefix := \"openlist_thumb_\"\n\tthumbName := thumbPrefix + utils.GetMD5EncodeStr(fullPath) + \".png\"\n\tif d.ThumbCacheFolder != \"\" {\n\t\t// skip if the file is a thumbnail\n\t\tif strings.HasPrefix(file.GetName(), thumbPrefix) {\n\t\t\treturn nil, &fullPath, nil\n\t\t}\n\t\tthumbPath := filepath.Join(d.ThumbCacheFolder, thumbName)\n\t\tif utils.Exists(thumbPath) {\n\t\t\treturn nil, &thumbPath, nil\n\t\t}\n\t}\n\tvar srcBuf *bytes.Buffer\n\tif utils.GetFileType(file.GetName()) == conf.VIDEO {\n\t\tvideoBuf, err := d.GetSnapshot(fullPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tsrcBuf = videoBuf\n\t} else {\n\t\timgData, err := os.ReadFile(fullPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\timgBuf := bytes.NewBuffer(imgData)\n\t\tsrcBuf = imgBuf\n\t}\n\n\timage, err := imaging.Decode(srcBuf, imaging.AutoOrientation(true))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tthumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos)\n\tvar buf bytes.Buffer\n\terr = imaging.Encode(&buf, thumbImg, imaging.PNG)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif d.ThumbCacheFolder != \"\" {\n\t\terr = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0o666)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\treturn &buf, nil, nil\n}\n\ntype DirectoryMap struct {\n\troot string\n\tdata sync.Map\n}\n\ntype DirectoryNode struct {\n\tfileSum      int64\n\tdirectorySum int64\n\tchildren     []string\n}\n\ntype DirectoryTask struct {\n\tpath  string\n\tcache *DirectoryTaskCache\n}\n\ntype DirectoryTaskCache struct {\n\tfileSum  int64\n\tchildren []string\n}\n\nfunc (m *DirectoryMap) Has(path string) bool {\n\t_, ok := m.data.Load(path)\n\n\treturn ok\n}\n\nfunc (m *DirectoryMap) Get(path string) (*DirectoryNode, bool) {\n\tvalue, ok := m.data.Load(path)\n\tif !ok {\n\t\treturn &DirectoryNode{}, false\n\t}\n\n\tnode, ok := value.(*DirectoryNode)\n\tif !ok {\n\t\treturn &DirectoryNode{}, false\n\t}\n\n\treturn node, true\n}\n\nfunc (m *DirectoryMap) Set(path string, node *DirectoryNode) {\n\tm.data.Store(path, node)\n}\n\nfunc (m *DirectoryMap) Delete(path string) {\n\tm.data.Delete(path)\n}\n\nfunc (m *DirectoryMap) Clear() {\n\tm.data.Clear()\n}\n\nfunc (m *DirectoryMap) RecalculateDirSize() error {\n\tm.Clear()\n\tif m.root == \"\" {\n\t\treturn fmt.Errorf(\"root path is not set\")\n\t}\n\n\tsize, err := m.CalculateDirSize(m.root)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif node, ok := m.Get(m.root); ok {\n\t\tnode.fileSum = size\n\t\tnode.directorySum = size\n\t}\n\n\treturn nil\n}\n\nfunc (m *DirectoryMap) CalculateDirSize(dirname string) (int64, error) {\n\tstack := []DirectoryTask{\n\t\t{path: dirname},\n\t}\n\n\tfor len(stack) > 0 {\n\t\ttask := stack[len(stack)-1]\n\t\tstack = stack[:len(stack)-1]\n\n\t\tif task.cache != nil {\n\t\t\tdirectorySum := int64(0)\n\n\t\t\tfor _, filename := range task.cache.children {\n\t\t\t\tchild, ok := m.Get(filepath.Join(task.path, filename))\n\t\t\t\tif !ok {\n\t\t\t\t\treturn 0, fmt.Errorf(\"child node not found\")\n\t\t\t\t}\n\t\t\t\tdirectorySum += child.fileSum + child.directorySum\n\t\t\t}\n\n\t\t\tm.Set(task.path, &DirectoryNode{\n\t\t\t\tfileSum:      task.cache.fileSum,\n\t\t\t\tdirectorySum: directorySum,\n\t\t\t\tchildren:     task.cache.children,\n\t\t\t})\n\n\t\t\tcontinue\n\t\t}\n\n\t\tfiles, err := readDir(task.path)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tfileSum := int64(0)\n\t\tdirectorySum := int64(0)\n\n\t\tchildren := []string{}\n\t\tqueue := []DirectoryTask{}\n\n\t\tfor _, f := range files {\n\t\t\tfullpath := filepath.Join(task.path, f.Name())\n\t\t\tisFolder := f.IsDir() || isSymlinkDir(f, fullpath)\n\n\t\t\tif isFolder {\n\t\t\t\tif node, ok := m.Get(fullpath); ok {\n\t\t\t\t\tdirectorySum += node.fileSum + node.directorySum\n\t\t\t\t} else {\n\t\t\t\t\tqueue = append(queue, DirectoryTask{\n\t\t\t\t\t\tpath: fullpath,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tchildren = append(children, f.Name())\n\t\t\t} else {\n\t\t\t\tfileSum += f.Size()\n\t\t\t}\n\t\t}\n\n\t\tif len(queue) > 0 {\n\t\t\tstack = append(stack, DirectoryTask{\n\t\t\t\tpath: task.path,\n\t\t\t\tcache: &DirectoryTaskCache{\n\t\t\t\t\tfileSum:  fileSum,\n\t\t\t\t\tchildren: children,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tstack = append(stack, queue...)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tm.Set(task.path, &DirectoryNode{\n\t\t\tfileSum:      fileSum,\n\t\t\tdirectorySum: directorySum,\n\t\t\tchildren:     children,\n\t\t})\n\t}\n\n\tif node, ok := m.Get(dirname); ok {\n\t\treturn node.fileSum + node.directorySum, nil\n\t}\n\n\treturn 0, nil\n}\n\nfunc (m *DirectoryMap) UpdateDirSize(dirname string) (int64, error) {\n\tnode, ok := m.Get(dirname)\n\tif !ok {\n\t\treturn 0, fmt.Errorf(\"directory node not found\")\n\t}\n\n\tfiles, err := readDir(dirname)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tfileSum := int64(0)\n\tdirectorySum := int64(0)\n\n\tchildren := []string{}\n\n\tfor _, f := range files {\n\t\tfullpath := filepath.Join(dirname, f.Name())\n\t\tisFolder := f.IsDir() || isSymlinkDir(f, fullpath)\n\n\t\tif isFolder {\n\t\t\tif node, ok := m.Get(fullpath); ok {\n\t\t\t\tdirectorySum += node.fileSum + node.directorySum\n\t\t\t} else {\n\t\t\t\tvalue, err := m.CalculateDirSize(fullpath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn 0, err\n\t\t\t\t}\n\t\t\t\tdirectorySum += value\n\t\t\t}\n\n\t\t\tchildren = append(children, f.Name())\n\t\t} else {\n\t\t\tfileSum += f.Size()\n\t\t}\n\t}\n\n\tfor _, c := range node.children {\n\t\tif !slices.Contains(children, c) {\n\t\t\tm.DeleteDirNode(filepath.Join(dirname, c))\n\t\t}\n\t}\n\n\tnode.fileSum = fileSum\n\tnode.directorySum = directorySum\n\tnode.children = children\n\n\treturn fileSum + directorySum, nil\n}\n\nfunc (m *DirectoryMap) UpdateDirParents(dirname string) error {\n\tparentPath := filepath.Dir(dirname)\n\tfor parentPath != m.root && !strings.HasPrefix(m.root, parentPath) {\n\t\tif node, ok := m.Get(parentPath); ok {\n\t\t\tdirectorySum := int64(0)\n\n\t\t\tfor _, c := range node.children {\n\t\t\t\tchild, ok := m.Get(filepath.Join(parentPath, c))\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"child node not found\")\n\t\t\t\t}\n\t\t\t\tdirectorySum += child.fileSum + child.directorySum\n\t\t\t}\n\n\t\t\tnode.directorySum = directorySum\n\t\t}\n\n\t\tparentPath = filepath.Dir(parentPath)\n\t}\n\n\treturn nil\n}\n\nfunc (m *DirectoryMap) DeleteDirNode(dirname string) error {\n\tstack := []string{dirname}\n\n\tfor len(stack) > 0 {\n\t\tcurrent := stack[len(stack)-1]\n\t\tstack = stack[:len(stack)-1]\n\n\t\tif node, ok := m.Get(current); ok {\n\t\t\tfor _, filename := range node.children {\n\t\t\t\tstack = append(stack, filepath.Join(current, filename))\n\t\t\t}\n\n\t\t\tm.Delete(current)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Local) tryCopy(srcPath, dstPath string, info os.FileInfo) error {\n\tif info.Mode()&os.ModeDevice != 0 {\n\t\treturn errors.New(\"cannot copy a device\")\n\t} else if info.Mode()&os.ModeSymlink != 0 {\n\t\treturn d.copySymlink(srcPath, dstPath)\n\t} else if info.Mode()&os.ModeNamedPipe != 0 {\n\t\treturn copyNamedPipe(dstPath, info.Mode(), os.FileMode(d.mkdirPerm))\n\t} else if info.IsDir() {\n\t\treturn d.recurAndTryCopy(srcPath, dstPath)\n\t} else {\n\t\treturn tryReflinkCopy(srcPath, dstPath)\n\t}\n}\n\nfunc (d *Local) copySymlink(srcPath, dstPath string) error {\n\tlinkOrig, err := os.Readlink(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstDir := filepath.Dir(dstPath)\n\tif !filepath.IsAbs(linkOrig) {\n\t\tsrcDir := filepath.Dir(srcPath)\n\t\trel, err := filepath.Rel(dstDir, srcDir)\n\t\tif err != nil {\n\t\t\trel, err = filepath.Abs(srcDir)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlinkOrig = filepath.Clean(filepath.Join(rel, linkOrig))\n\t}\n\terr = os.MkdirAll(dstDir, os.FileMode(d.mkdirPerm))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.Symlink(linkOrig, dstPath)\n}\n\nfunc (d *Local) recurAndTryCopy(srcPath, dstPath string) error {\n\terr := os.MkdirAll(dstPath, os.FileMode(d.mkdirPerm))\n\tif err != nil {\n\t\treturn err\n\t}\n\tfiles, err := readDir(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range files {\n\t\tif !f.IsDir() {\n\t\t\tsp := filepath.Join(srcPath, f.Name())\n\t\t\tdp := filepath.Join(dstPath, f.Name())\n\t\t\tif err = d.tryCopy(sp, dp, f); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tfor _, f := range files {\n\t\tif f.IsDir() {\n\t\t\tsp := filepath.Join(srcPath, f.Name())\n\t\t\tdp := filepath.Join(dstPath, f.Name())\n\t\t\tif err = d.recurAndTryCopy(sp, dp); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc tryReflinkCopy(srcPath, dstPath string) error {\n\terr := reflink.Always(srcPath, dstPath)\n\tif errors.Is(err, reflink.ErrReflinkUnsupported) || errors.Is(err, reflink.ErrReflinkFailed) || isCrossDeviceError(err) {\n\t\treturn errs.NotImplement\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "drivers/local/util_unix.go",
    "content": "//go:build !windows\n\npackage local\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc isHidden(f fs.FileInfo, _ string) bool {\n\treturn strings.HasPrefix(f.Name(), \".\")\n}\n\nfunc getDiskUsage(path string) (model.DiskUsage, error) {\n\tvar stat syscall.Statfs_t\n\terr := syscall.Statfs(path, &stat)\n\tif err != nil {\n\t\treturn model.DiskUsage{}, err\n\t}\n\ttotal := int64(stat.Blocks) * int64(stat.Bsize)\n\tfree := int64(stat.Bfree) * int64(stat.Bsize)\n\treturn model.DiskUsage{\n\t\tTotalSpace: total,\n\t\tUsedSpace:  total - free,\n\t}, nil\n}\n\nfunc isCrossDeviceError(err error) bool {\n\treturn errors.Is(err, unix.EXDEV)\n}\n"
  },
  {
    "path": "drivers/local/util_windows.go",
    "content": "//go:build windows\n\npackage local\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"syscall\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc isHidden(f fs.FileInfo, fullPath string) bool {\n\tfilePath := filepath.Join(fullPath, f.Name())\n\tnamePtr, err := syscall.UTF16PtrFromString(filePath)\n\tif err != nil {\n\t\treturn false\n\t}\n\tattrs, err := syscall.GetFileAttributes(namePtr)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0\n}\n\nfunc getDiskUsage(path string) (model.DiskUsage, error) {\n\tabs, err := filepath.Abs(path)\n\tif err != nil {\n\t\treturn model.DiskUsage{}, err\n\t}\n\troot := filepath.VolumeName(abs)\n\tif len(root) != 2 || root[1] != ':' {\n\t\treturn model.DiskUsage{}, errors.New(\"cannot get disk label\")\n\t}\n\tvar freeBytes, totalBytes, totalFreeBytes uint64\n\terr = windows.GetDiskFreeSpaceEx(\n\t\twindows.StringToUTF16Ptr(root),\n\t\t&freeBytes,\n\t\t&totalBytes,\n\t\t&totalFreeBytes,\n\t)\n\tif err != nil {\n\t\treturn model.DiskUsage{}, err\n\t}\n\treturn model.DiskUsage{\n\t\tTotalSpace: int64(totalBytes),\n\t\tUsedSpace:  int64(totalBytes - freeBytes),\n\t}, nil\n}\n\nfunc isCrossDeviceError(err error) bool {\n\treturn errors.Is(err, windows.ERROR_NOT_SAME_DEVICE)\n}\n"
  },
  {
    "path": "drivers/mediafire/driver.go",
    "content": "package mediafire\n\n/*\nPackage mediafire\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-11\n\nD@' 3z K!7 - The King Of Cracking\n\nModifications by ILoveScratch2<ilovescratch@foxmail.com>\nDate: 2025-09-21\n\nDate: 2025-09-26\nFinal opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7\n*/\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Mediafire struct {\n\tmodel.Storage\n\tAddition\n\n\tcron *cron.Cron\n\n\tactionToken string\n\tlimiter     *rate.Limiter\n\n\tappBase    string\n\tapiBase    string\n\thostBase   string\n\tmaxRetries int\n\n\tsecChUa         string\n\tsecChUaPlatform string\n\tuserAgent       string\n}\n\nfunc (d *Mediafire) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Mediafire) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\n// Init initializes the MediaFire driver with session token and cookie validation\nfunc (d *Mediafire) Init(ctx context.Context) error {\n\tif d.Cookie == \"\" {\n\t\treturn fmt.Errorf(\"Init :: [MediaFire] {critical} missing Cookie\")\n\t}\n\n\t// If SessionToken is empty, try to get it from cookie\n\tif d.SessionToken == \"\" {\n\t\tif _, err := d.getSessionToken(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"Init :: [MediaFire] {critical} failed to get session token from cookie: %w\", err)\n\t\t}\n\t}\n\n\t// Setup rate limiter if rate limit is configured\n\tif d.LimitRate > 0 {\n\t\td.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)\n\t}\n\n\t// Validate and refresh session token if needed\n\tif _, err := d.getSessionToken(ctx); err != nil {\n\t\td.renewToken(ctx)\n\n\t\t// Avoids 10 mins token expiry (6- 9)\n\t\tnum := rand.Intn(4) + 6\n\n\t\td.cron = cron.NewCron(time.Minute * time.Duration(num))\n\t\td.cron.Do(func() {\n\t\t\t// Crazy, but working way to refresh session token\n\t\t\td.renewToken(ctx)\n\t\t})\n\n\t}\n\n\treturn nil\n}\n\n// Drop cleans up driver resources\nfunc (d *Mediafire) Drop(ctx context.Context) error {\n\t// Clear cached resources\n\td.actionToken = \"\"\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t\td.cron = nil\n\t}\n\treturn nil\n}\n\n// List retrieves files and folders from the specified directory\nfunc (d *Mediafire) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn d.fileToObj(src), nil\n\t})\n}\n\n// Link generates a direct download link for the specified file\nfunc (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdownloadUrl, err := d.getDirectDownloadLink(ctx, file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Head(downloadUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\t_ = res.RawBody().Close()\n\t}()\n\n\tif res.StatusCode() == 302 {\n\t\tdownloadUrl = res.Header().Get(\"location\")\n\t}\n\n\treturn &model.Link{\n\t\tURL: downloadUrl,\n\t\tHeader: http.Header{\n\t\t\t\"Origin\":             []string{d.appBase},\n\t\t\t\"Referer\":            []string{d.appBase + \"/\"},\n\t\t\t\"sec-ch-ua\":          []string{d.secChUa},\n\t\t\t\"sec-ch-ua-platform\": []string{d.secChUaPlatform},\n\t\t\t\"User-Agent\":         []string{d.userAgent},\n\t\t},\n\t}, nil\n}\n\n// MakeDir creates a new folder in the specified parent directory\nfunc (d *Mediafire) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tdata := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"response_format\": \"json\",\n\t\t\"parent_key\":      parentDir.GetID(),\n\t\t\"foldername\":      dirName,\n\t}\n\n\tvar resp MediafireFolderCreateResponse\n\t_, err := d.postForm(ctx, \"/folder/create.php\", data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreated, _ := time.Parse(\"2006-01-02T15:04:05Z\", resp.Response.CreatedUTC)\n\n\treturn &model.Object{\n\t\tID:       resp.Response.FolderKey,\n\t\tName:     resp.Response.Name,\n\t\tSize:     0,\n\t\tModified: created,\n\t\tCtime:    created,\n\t\tIsFolder: true,\n\t}, nil\n}\n\n// Move relocates a file or folder to a different parent directory\nfunc (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar data map[string]string\n\tvar endpoint string\n\n\tif srcObj.IsDir() {\n\n\t\tendpoint = \"/folder/move.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"folder_key_src\":  srcObj.GetID(),\n\t\t\t\"folder_key_dst\":  dstDir.GetID(),\n\t\t}\n\t} else {\n\n\t\tendpoint = \"/file/move.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"quick_key\":       srcObj.GetID(),\n\t\t\t\"folder_key\":      dstDir.GetID(),\n\t\t}\n\t}\n\n\tvar resp MediafireMoveResponse\n\t_, err := d.postForm(ctx, endpoint, data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn srcObj, nil\n}\n\n// Rename changes the name of a file or folder\nfunc (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tvar data map[string]string\n\tvar endpoint string\n\n\tif srcObj.IsDir() {\n\n\t\tendpoint = \"/folder/update.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"folder_key\":      srcObj.GetID(),\n\t\t\t\"foldername\":      newName,\n\t\t}\n\t} else {\n\n\t\tendpoint = \"/file/update.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"quick_key\":       srcObj.GetID(),\n\t\t\t\"filename\":        newName,\n\t\t}\n\t}\n\n\tvar resp MediafireRenameResponse\n\t_, err := d.postForm(ctx, endpoint, data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       srcObj.GetID(),\n\t\tName:     newName,\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: srcObj.ModTime(),\n\t\tCtime:    srcObj.CreateTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\n// Copy creates a duplicate of a file or folder in the specified destination directory\nfunc (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar data map[string]string\n\tvar endpoint string\n\n\tif srcObj.IsDir() {\n\n\t\tendpoint = \"/folder/copy.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"folder_key_src\":  srcObj.GetID(),\n\t\t\t\"folder_key_dst\":  dstDir.GetID(),\n\t\t}\n\t} else {\n\n\t\tendpoint = \"/file/copy.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"quick_key\":       srcObj.GetID(),\n\t\t\t\"folder_key\":      dstDir.GetID(),\n\t\t}\n\t}\n\n\tvar resp MediafireCopyResponse\n\t_, err := d.postForm(ctx, endpoint, data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar newID string\n\tif srcObj.IsDir() {\n\t\tif len(resp.Response.NewFolderKeys) > 0 {\n\t\t\tnewID = resp.Response.NewFolderKeys[0]\n\t\t}\n\t} else {\n\t\tif len(resp.Response.NewQuickKeys) > 0 {\n\t\t\tnewID = resp.Response.NewQuickKeys[0]\n\t\t}\n\t}\n\n\treturn &model.Object{\n\t\tID:       newID,\n\t\tName:     srcObj.GetName(),\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: srcObj.ModTime(),\n\t\tCtime:    srcObj.CreateTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\n// Remove deletes a file or folder permanently\nfunc (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error {\n\tvar data map[string]string\n\tvar endpoint string\n\n\tif obj.IsDir() {\n\n\t\tendpoint = \"/folder/delete.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"folder_key\":      obj.GetID(),\n\t\t}\n\t} else {\n\n\t\tendpoint = \"/file/delete.php\"\n\t\tdata = map[string]string{\n\t\t\t\"session_token\":   d.SessionToken,\n\t\t\t\"response_format\": \"json\",\n\t\t\t\"quick_key\":       obj.GetID(),\n\t\t}\n\t}\n\n\tvar resp MediafireRemoveResponse\n\t_, err := d.postForm(ctx, endpoint, data, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn checkAPIResult(resp.Response.Result)\n}\n\n// Put uploads a file to the specified directory with support for resumable upload and quick upload\nfunc (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tfileHash := file.GetHash().GetHash(utils.SHA256)\n\tvar err error\n\n\t// Try to use existing hash first, cache only if necessary\n\tif len(fileHash) != utils.SHA256.Width {\n\t\t_, fileHash, err = stream.CacheFullAndHash(file, &up, utils.SHA256)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcheckResp, err := d.uploadCheck(ctx, file.GetName(), file.GetSize(), fileHash, dstDir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif checkResp.Response.HashExists == \"yes\" && checkResp.Response.InAccount == \"yes\" {\n\t\tup(100.0)\n\t\texistingFile, err := d.getExistingFileInfo(ctx, fileHash, file.GetName(), dstDir.GetID())\n\t\tif err == nil && existingFile != nil {\n\t\t\t// File exists, return existing file info\n\t\t\treturn &model.Object{\n\t\t\t\tID:   existingFile.GetID(),\n\t\t\t\tName: file.GetName(),\n\t\t\t\tSize: file.GetSize(),\n\t\t\t}, nil\n\t\t}\n\t\t// If getExistingFileInfo fails, log and continue with normal upload\n\t\t// This ensures upload doesn't fail due to search issues\n\t}\n\n\tvar pollKey string\n\n\tif checkResp.Response.ResumableUpload.AllUnitsReady != \"yes\" {\n\t\tpollKey, err = d.uploadUnits(ctx, file, checkResp, file.GetName(), fileHash, dstDir.GetID(), up)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tpollKey = checkResp.Response.ResumableUpload.UploadKey\n\t}\n\tdefer up(100.0)\n\n\tpollResp, err := d.pollUpload(ctx, pollKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:   pollResp.Response.Doupload.QuickKey,\n\t\tName: file.GetName(),\n\t\tSize: file.GetSize(),\n\t}, nil\n}\n\nfunc (d *Mediafire) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tdata := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"response_format\": \"json\",\n\t}\n\tvar resp MediafireUserInfoResponse\n\t_, err := d.postForm(ctx, \"/user/get_info.php\", data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(resp.Response.UserInfo.UsedStorageSize, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := strconv.ParseInt(resp.Response.UserInfo.StorageLimit, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Mediafire)(nil)\n"
  },
  {
    "path": "drivers/mediafire/meta.go",
    "content": "package mediafire\n\n/*\nPackage mediafire\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-11\n\nD@' 3z K!7 - The King Of Cracking\n\nModifications by ILoveScratch2<ilovescratch@foxmail.com>\nDate: 2025-09-21\n\nDate: 2025-09-26\nFinal opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7\n*/\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\t//driver.RootID\n\n\tSessionToken string `json:\"session_token\" required:\"false\" type:\"string\" help:\"Optional for MediaFire API, can be auto-acquired from cookie\"`\n\tCookie       string `json:\"cookie\" required:\"true\" type:\"string\" help:\"Required for MediaFire API authentication\"`\n\n\tOrderBy        string  `json:\"order_by\" type:\"select\" options:\"name,time,size\" default:\"name\"`\n\tOrderDirection string  `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tChunkSize      int64   `json:\"chunk_size\" type:\"number\" default:\"100\"`\n\tUploadThreads  int     `json:\"upload_threads\" type:\"number\" default:\"3\" help:\"concurrent upload threads\"`\n\tLimitRate      float64 `json:\"limit_rate\" type:\"float\" default:\"2\" help:\"limit all api request rate ([limit]r/1s)\"`\n}\n\nvar config = driver.Config{\n\tName:              \"MediaFire\",\n\tLocalSort:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Mediafire{\n\t\t\tappBase:    \"https://app.mediafire.com\",\n\t\t\tapiBase:    \"https://www.mediafire.com/api/1.5\",\n\t\t\thostBase:   \"https://www.mediafire.com\",\n\t\t\tmaxRetries: 3,\n\t\t\tuserAgent:  base.UserAgent,\n\t\t}\n\t})\n}\n\n"
  },
  {
    "path": "drivers/mediafire/types.go",
    "content": "package mediafire\n\n/*\nPackage mediafire\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-11\n\nD@' 3z K!7 - The King Of Cracking\n*/\n\ntype MediafireRenewTokenResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tSessionToken      string `json:\"session_token\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireResponse struct {\n\tResponse struct {\n\t\tAction        string `json:\"action\"`\n\t\tFolderContent struct {\n\t\t\tChunkSize   string            `json:\"chunk_size\"`\n\t\t\tContentType string            `json:\"content_type\"`\n\t\t\tChunkNumber string            `json:\"chunk_number\"`\n\t\t\tFolderKey   string            `json:\"folderkey\"`\n\t\t\tFolders     []MediafireFolder `json:\"folders,omitempty\"`\n\t\t\tFiles       []MediafireFile   `json:\"files,omitempty\"`\n\t\t\tMoreChunks  string            `json:\"more_chunks\"`\n\t\t} `json:\"folder_content\"`\n\t\tResult string `json:\"result\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireFolder struct {\n\tFolderKey  string `json:\"folderkey\"`\n\tName       string `json:\"name\"`\n\tCreated    string `json:\"created\"`\n\tCreatedUTC string `json:\"created_utc\"`\n}\n\ntype MediafireFile struct {\n\tQuickKey   string `json:\"quickkey\"`\n\tFilename   string `json:\"filename\"`\n\tSize       string `json:\"size\"`\n\tCreated    string `json:\"created\"`\n\tCreatedUTC string `json:\"created_utc\"`\n\tMimeType   string `json:\"mimetype\"`\n}\n\ntype File struct {\n\tID         string\n\tName       string\n\tSize       int64\n\tCreatedUTC string\n\tIsFolder   bool\n}\n\ntype FolderContentResponse struct {\n\tFolders    []MediafireFolder\n\tFiles      []MediafireFile\n\tMoreChunks bool\n}\n\ntype MediafireLinksResponse struct {\n\tResponse struct {\n\t\tAction string `json:\"action\"`\n\t\tLinks  []struct {\n\t\t\tQuickKey       string `json:\"quickkey\"`\n\t\t\tView           string `json:\"view\"`\n\t\t\tNormalDownload string `json:\"normal_download\"`\n\t\t\tOneTime        struct {\n\t\t\t\tDownload string `json:\"download\"`\n\t\t\t\tView     string `json:\"view\"`\n\t\t\t} `json:\"one_time\"`\n\t\t} `json:\"links\"`\n\t\tOneTimeKeyRequestCount    string `json:\"one_time_key_request_count\"`\n\t\tOneTimeKeyRequestMaxCount string `json:\"one_time_key_request_max_count\"`\n\t\tResult                    string `json:\"result\"`\n\t\tCurrentAPIVersion         string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireDirectDownloadResponse struct {\n\tResponse struct {\n\t\tAction string `json:\"action\"`\n\t\tLinks  []struct {\n\t\t\tQuickKey       string `json:\"quickkey\"`\n\t\t\tDirectDownload string `json:\"direct_download\"`\n\t\t} `json:\"links\"`\n\t\tDirectDownloadFreeBandwidth string `json:\"direct_download_free_bandwidth\"`\n\t\tResult                      string `json:\"result\"`\n\t\tCurrentAPIVersion           string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireFolderCreateResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tFolderKey         string `json:\"folder_key\"`\n\t\tUploadKey         string `json:\"upload_key\"`\n\t\tParentFolderKey   string `json:\"parent_folderkey\"`\n\t\tName              string `json:\"name\"`\n\t\tDescription       string `json:\"description\"`\n\t\tCreated           string `json:\"created\"`\n\t\tCreatedUTC        string `json:\"created_utc\"`\n\t\tPrivacy           string `json:\"privacy\"`\n\t\tFileCount         string `json:\"file_count\"`\n\t\tFolderCount       string `json:\"folder_count\"`\n\t\tRevision          string `json:\"revision\"`\n\t\tDropboxEnabled    string `json:\"dropbox_enabled\"`\n\t\tFlag              string `json:\"flag\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t\tNewDeviceRevision int    `json:\"new_device_revision\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireMoveResponse struct {\n\tResponse struct {\n\t\tAction            string   `json:\"action\"`\n\t\tAsynchronous      string   `json:\"asynchronous,omitempty\"`\n\t\tNewNames          []string `json:\"new_names\"`\n\t\tResult            string   `json:\"result\"`\n\t\tCurrentAPIVersion string   `json:\"current_api_version\"`\n\t\tNewDeviceRevision int      `json:\"new_device_revision\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireRenameResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tAsynchronous      string `json:\"asynchronous,omitempty\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t\tNewDeviceRevision int    `json:\"new_device_revision\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireCopyResponse struct {\n\tResponse struct {\n\t\tAction            string   `json:\"action\"`\n\t\tAsynchronous      string   `json:\"asynchronous,omitempty\"`\n\t\tNewQuickKeys      []string `json:\"new_quickkeys,omitempty\"`\n\t\tNewFolderKeys     []string `json:\"new_folderkeys,omitempty\"`\n\t\tSkippedCount      string   `json:\"skipped_count,omitempty\"`\n\t\tOtherCount        string   `json:\"other_count,omitempty\"`\n\t\tResult            string   `json:\"result\"`\n\t\tCurrentAPIVersion string   `json:\"current_api_version\"`\n\t\tNewDeviceRevision int      `json:\"new_device_revision\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireRemoveResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tAsynchronous      string `json:\"asynchronous,omitempty\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t\tNewDeviceRevision int    `json:\"new_device_revision\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireCheckResponse struct {\n\tResponse struct {\n\t\tAction          string `json:\"action\"`\n\t\tHashExists      string `json:\"hash_exists\"`\n\t\tInAccount       string `json:\"in_account\"`\n\t\tInFolder        string `json:\"in_folder\"`\n\t\tFileExists      string `json:\"file_exists\"`\n\t\tResumableUpload struct {\n\t\t\tAllUnitsReady string `json:\"all_units_ready\"`\n\t\t\tNumberOfUnits string `json:\"number_of_units\"`\n\t\t\tUnitSize      string `json:\"unit_size\"`\n\t\t\tBitmap        struct {\n\t\t\t\tCount string   `json:\"count\"`\n\t\t\t\tWords []string `json:\"words\"`\n\t\t\t} `json:\"bitmap\"`\n\t\t\tUploadKey string `json:\"upload_key\"`\n\t\t} `json:\"resumable_upload\"`\n\t\tAvailableSpace       string `json:\"available_space\"`\n\t\tUsedStorageSize      string `json:\"used_storage_size\"`\n\t\tStorageLimit         string `json:\"storage_limit\"`\n\t\tStorageLimitExceeded string `json:\"storage_limit_exceeded\"`\n\t\tUploadURL            struct {\n\t\t\tSimple            string `json:\"simple\"`\n\t\t\tSimpleFallback    string `json:\"simple_fallback\"`\n\t\t\tResumable         string `json:\"resumable\"`\n\t\t\tResumableFallback string `json:\"resumable_fallback\"`\n\t\t} `json:\"upload_url\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\ntype MediafireActionTokenResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tActionToken       string `json:\"action_token\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafirePollResponse struct {\n\tResponse struct {\n\t\tAction   string `json:\"action\"`\n\t\tDoupload struct {\n\t\t\tResult      string `json:\"result\"`\n\t\t\tStatus      string `json:\"status\"`\n\t\t\tDescription string `json:\"description\"`\n\t\t\tQuickKey    string `json:\"quickkey\"`\n\t\t\tHash        string `json:\"hash\"`\n\t\t\tFilename    string `json:\"filename\"`\n\t\t\tSize        string `json:\"size\"`\n\t\t\tCreated     string `json:\"created\"`\n\t\t\tCreatedUTC  string `json:\"created_utc\"`\n\t\t\tRevision    string `json:\"revision\"`\n\t\t} `json:\"doupload\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireFileSearchResponse struct {\n\tResponse struct {\n\t\tAction            string `json:\"action\"`\n\t\tFileInfo          []File `json:\"file_info\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\ntype MediafireUserInfoResponse struct {\n\tResponse struct {\n\t\tAction   string `json:\"action\"`\n\t\tUserInfo struct {\n\t\t\tEmail           string `json:\"string\"`\n\t\t\tDisplayName     string `json:\"display_name\"`\n\t\t\tUsedStorageSize string `json:\"used_storage_size\"`\n\t\t\tStorageLimit    string `json:\"storage_limit\"`\n\t\t} `json:\"user_info\"`\n\t\tResult            string `json:\"result\"`\n\t\tCurrentAPIVersion string `json:\"current_api_version\"`\n\t} `json:\"response\"`\n}\n\n"
  },
  {
    "path": "drivers/mediafire/util.go",
    "content": "package mediafire\n\n/*\nPackage mediafire\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-11\n\nD@' 3z K!7 - The King Of Cracking\n\nModifications by ILoveScratch2<ilovescratch@foxmail.com>\nDate: 2025-09-21\n\nDate: 2025-09-26\nFinal opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7\n*/\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\n// checkAPIResult validates MediaFire API response result and returns error if not successful\nfunc checkAPIResult(result string) error {\n\tif result != \"Success\" {\n\t\treturn fmt.Errorf(\"MediaFire API error: %s\", result)\n\t}\n\treturn nil\n}\n\n// getSessionToken retrieves and validates session token from MediaFire\nfunc (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {\n\tif d.limiter != nil {\n\t\tif err := d.limiter.Wait(ctx); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"rate limit wait failed: %w\", err)\n\t\t}\n\t}\n\n\ttokenURL := d.hostBase + \"/application/get_session_token.php\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Set(\"Accept\", \"*/*\")\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"Accept-Language\", \"en-US,en;q=0.9\")\n\treq.Header.Set(\"Content-Length\", \"0\")\n\treq.Header.Set(\"Cookie\", d.Cookie)\n\treq.Header.Set(\"DNT\", \"1\")\n\treq.Header.Set(\"Origin\", d.hostBase)\n\treq.Header.Set(\"Priority\", \"u=1, i\")\n\treq.Header.Set(\"Referer\", (d.hostBase + \"/\"))\n\treq.Header.Set(\"Sec-Ch-Ua\", d.secChUa)\n\treq.Header.Set(\"Sec-Ch-Ua-Mobile\", \"?0\")\n\treq.Header.Set(\"Sec-Ch-Ua-Platform\", d.secChUaPlatform)\n\treq.Header.Set(\"Sec-Fetch-Dest\", \"empty\")\n\treq.Header.Set(\"Sec-Fetch-Mode\", \"cors\")\n\treq.Header.Set(\"Sec-Fetch-Site\", \"same-site\")\n\treq.Header.Set(\"User-Agent\", d.userAgent)\n\t// req.Header.Set(\"Connection\", \"keep-alive\")\n\n\tresp, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar body []byte\n\t// Handle gzip decompression if needed\n\tif resp.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t\tgzipReader, err := gzip.NewReader(resp.Body)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create gzip reader: %w\", err)\n\t\t}\n\t\tdefer gzipReader.Close()\n\t\tbody, _ = io.ReadAll(gzipReader)\n\t} else {\n\t\tbody, err = io.ReadAll(resp.Body)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// fmt.Printf(\"getSessionToken :: Raw response: %s\\n\", string(body))\n\t// fmt.Printf(\"getSessionToken :: Parsed response: %+v\\n\", resp)\n\n\tvar tokenResp struct {\n\t\tResponse struct {\n\t\t\tSessionToken string `json:\"session_token\"`\n\t\t} `json:\"response\"`\n\t}\n\n\tif resp.StatusCode == 200 {\n\t\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif tokenResp.Response.SessionToken == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"empty session token received\")\n\t\t}\n\n\t\tcookieMap := make(map[string]string)\n\t\tfor _, cookie := range resp.Cookies() {\n\t\t\tcookieMap[cookie.Name] = cookie.Value\n\t\t}\n\n\t\tif len(cookieMap) > 0 {\n\n\t\t\tvar cookies []string\n\t\t\tfor name, value := range cookieMap {\n\t\t\t\tcookies = append(cookies, fmt.Sprintf(\"%s=%s\", name, value))\n\t\t\t}\n\t\t\td.Cookie = strings.Join(cookies, \"; \")\n\t\t\top.MustSaveDriverStorage(d)\n\n\t\t\t// fmt.Printf(\"getSessionToken :: Captured cookies: %s\\n\", d.Cookie)\n\t\t}\n\n\t} else {\n\t\treturn \"\", fmt.Errorf(\"getSessionToken :: failed to get session token, status code: %d\", resp.StatusCode)\n\t}\n\n\td.SessionToken = tokenResp.Response.SessionToken\n\n\t// fmt.Printf(\"Init :: Obtain Session Token %v\", d.SessionToken)\n\n\top.MustSaveDriverStorage(d)\n\n\treturn d.SessionToken, nil\n}\n\n// renewToken refreshes the current session token when expired\nfunc (d *Mediafire) renewToken(ctx context.Context) error {\n\tquery := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"response_format\": \"json\",\n\t}\n\n\tvar resp MediafireRenewTokenResponse\n\t_, err := d.postForm(ctx, \"/user/renew_session_token.php\", query, &resp)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to renew token: %w\", err)\n\t}\n\n\t// fmt.Printf(\"getInfo :: Raw response: %s\\n\", string(body))\n\t// fmt.Printf(\"getInfo :: Parsed response: %+v\\n\", resp)\n\n\tif resp.Response.Result != \"Success\" {\n\t\treturn fmt.Errorf(\"MediaFire token renewal failed: %s\", resp.Response.Result)\n\t}\n\n\td.SessionToken = resp.Response.SessionToken\n\n\t// fmt.Printf(\"Init :: Renew Session Token: %s\", resp.Response.Result)\n\n\top.MustSaveDriverStorage(d)\n\n\treturn nil\n}\n\nfunc (d *Mediafire) getFiles(ctx context.Context, folderKey string) ([]File, error) {\n\t// Pre-allocate slice with reasonable capacity to reduce memory allocations\n\tfiles := make([]File, 0, d.ChunkSize*2) // Estimate: ChunkSize for files + folders\n\thasMore := true\n\tchunkNumber := 1\n\n\tfor hasMore {\n\t\tresp, err := d.getFolderContent(ctx, folderKey, chunkNumber)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Process folders and files in single loop to improve cache locality\n\t\ttotalItems := len(resp.Folders) + len(resp.Files)\n\t\tif cap(files)-len(files) < totalItems {\n\t\t\t// Grow slice if needed\n\t\t\tnewFiles := make([]File, len(files), len(files)+totalItems+int(d.ChunkSize))\n\t\t\tcopy(newFiles, files)\n\t\t\tfiles = newFiles\n\t\t}\n\n\t\tfor _, folder := range resp.Folders {\n\t\t\tfiles = append(files, File{\n\t\t\t\tID:         folder.FolderKey,\n\t\t\t\tName:       folder.Name,\n\t\t\t\tSize:       0,\n\t\t\t\tCreatedUTC: folder.CreatedUTC,\n\t\t\t\tIsFolder:   true,\n\t\t\t})\n\t\t}\n\n\t\tfor _, file := range resp.Files {\n\t\t\tsize, _ := strconv.ParseInt(file.Size, 10, 64)\n\t\t\tfiles = append(files, File{\n\t\t\t\tID:         file.QuickKey,\n\t\t\t\tName:       file.Filename,\n\t\t\t\tSize:       size,\n\t\t\t\tCreatedUTC: file.CreatedUTC,\n\t\t\t\tIsFolder:   false,\n\t\t\t})\n\t\t}\n\n\t\thasMore = resp.MoreChunks\n\t\tchunkNumber++\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chunkNumber int) (*FolderContentResponse, error) {\n\tfoldersResp, err := d.getFolderContentByType(ctx, folderKey, \"folders\", chunkNumber)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfilesResp, err := d.getFolderContentByType(ctx, folderKey, \"files\", chunkNumber)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &FolderContentResponse{\n\t\tFolders:    foldersResp.Response.FolderContent.Folders,\n\t\tFiles:      filesResp.Response.FolderContent.Files,\n\t\tMoreChunks: foldersResp.Response.FolderContent.MoreChunks == \"yes\" || filesResp.Response.FolderContent.MoreChunks == \"yes\",\n\t}, nil\n}\n\nfunc (d *Mediafire) getFolderContentByType(ctx context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) {\n\tdata := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"response_format\": \"json\",\n\t\t\"folder_key\":      folderKey,\n\t\t\"content_type\":    contentType,\n\t\t\"chunk\":           strconv.Itoa(chunkNumber),\n\t\t\"chunk_size\":      strconv.FormatInt(d.ChunkSize, 10),\n\t\t\"details\":         \"yes\",\n\t\t\"order_direction\": d.OrderDirection,\n\t\t\"order_by\":        d.OrderBy,\n\t\t\"filter\":          \"\",\n\t}\n\n\tvar resp MediafireResponse\n\t_, err := d.postForm(ctx, \"/folder/get_content.php\", data, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\n// fileToObj converts MediaFire file data to model.ObjThumb with thumbnail support\nfunc (d *Mediafire) fileToObj(f File) *model.ObjThumb {\n\tcreated, _ := time.Parse(\"2006-01-02T15:04:05Z\", f.CreatedUTC)\n\n\tvar thumbnailURL string\n\tif !f.IsFolder && f.ID != \"\" {\n\t\tthumbnailURL = d.hostBase + \"/convkey/acaa/\" + f.ID + \"3g.jpg\"\n\t}\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID: f.ID,\n\t\t\t// Path:     \"\",\n\t\t\tName:     f.Name,\n\t\t\tSize:     f.Size,\n\t\t\tModified: created,\n\t\t\tCtime:    created,\n\t\t\tIsFolder: f.IsFolder,\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: thumbnailURL,\n\t\t},\n\t}\n}\n\nfunc (d *Mediafire) setCommonHeaders(req *resty.Request) {\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":     d.Cookie,\n\t\t\"User-Agent\": d.userAgent,\n\t\t\"Origin\":     d.appBase,\n\t\t\"Referer\":    d.appBase + \"/\",\n\t})\n}\n\n// apiRequest performs HTTP request to MediaFire API with rate limiting and common headers\nfunc (d *Mediafire) apiRequest(ctx context.Context, method, endpoint string, queryParams, formData map[string]string, resp interface{}) ([]byte, error) {\n\tif d.limiter != nil {\n\t\tif err := d.limiter.Wait(ctx); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"rate limit wait failed: %w\", err)\n\t\t}\n\t}\n\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\td.setCommonHeaders(req)\n\n\t// Set query parameters for GET requests\n\tif queryParams != nil {\n\t\treq.SetQueryParams(queryParams)\n\t}\n\n\t// Set form data for POST requests\n\tif formData != nil {\n\t\treq.SetFormData(formData)\n\t\treq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\t// Set response object if provided\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\tvar res *resty.Response\n\tvar err error\n\n\t// Execute request based on method\n\tswitch method {\n\tcase \"GET\":\n\t\tres, err = req.Get(d.apiBase + endpoint)\n\tcase \"POST\":\n\t\tres, err = req.Post(d.apiBase + endpoint)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported HTTP method: %s\", method)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn res.Body(), nil\n}\n\nfunc (d *Mediafire) getForm(ctx context.Context, endpoint string, query map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.apiRequest(ctx, \"GET\", endpoint, query, nil, resp)\n}\n\nfunc (d *Mediafire) postForm(ctx context.Context, endpoint string, data map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.apiRequest(ctx, \"POST\", endpoint, nil, data, resp)\n}\n\nfunc (d *Mediafire) getDirectDownloadLink(ctx context.Context, fileID string) (string, error) {\n\tdata := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"quick_key\":       fileID,\n\t\t\"link_type\":       \"direct_download\",\n\t\t\"response_format\": \"json\",\n\t}\n\n\tvar resp MediafireDirectDownloadResponse\n\t_, err := d.getForm(ctx, \"/file/get_links.php\", data, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(resp.Response.Links) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no download links found\")\n\t}\n\n\treturn resp.Response.Links[0].DirectDownload, nil\n}\n\nfunc (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {\n\tactionToken, err := d.getActionToken(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get action token: %w\", err)\n\t}\n\n\tquery := map[string]string{\n\t\t\"session_token\":   actionToken, /* d.SessionToken */\n\t\t\"filename\":        filename,\n\t\t\"size\":            strconv.FormatInt(filesize, 10),\n\t\t\"hash\":            filehash,\n\t\t\"folder_key\":      folderKey,\n\t\t\"resumable\":       \"yes\",\n\t\t\"response_format\": \"json\",\n\t}\n\n\tvar resp MediafireCheckResponse\n\t_, err = d.postForm(ctx, \"/upload/check.php\", query, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// fmt.Printf(\"uploadCheck :: Raw response: %s\\n\", string(body))\n\t// fmt.Printf(\"uploadCheck :: Parsed response: %+v\\n\", resp)\n\n\t// fmt.Printf(\"uploadCheck :: ResumableUpload section: %+v\\n\", resp.Response.ResumableUpload)\n\t// fmt.Printf(\"uploadCheck :: Upload key specifically: '%s'\\n\", resp.Response.ResumableUpload.UploadKey)\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Mediafire) uploadUnits(ctx context.Context, file model.FileStreamer, checkResp *MediafireCheckResponse, filename, fileHash, folderKey string, up driver.UpdateProgress) (string, error) {\n\tunitSize, _ := strconv.ParseInt(checkResp.Response.ResumableUpload.UnitSize, 10, 64)\n\tnumUnits, _ := strconv.Atoi(checkResp.Response.ResumableUpload.NumberOfUnits)\n\tuploadKey := checkResp.Response.ResumableUpload.UploadKey\n\n\tstringWords := checkResp.Response.ResumableUpload.Bitmap.Words\n\tintWords := make([]int, 0, len(stringWords))\n\tfor _, word := range stringWords {\n\t\tif intWord, err := strconv.Atoi(word); err == nil {\n\t\t\tintWords = append(intWords, intWord)\n\t\t}\n\t}\n\n\t// Intelligent buffer sizing for large files\n\tbufferSize := int(unitSize)\n\tfileSize := file.GetSize()\n\n\t// Split in chunks\n\tif fileSize > d.ChunkSize*1024*1024 {\n\n\t\t// Large, use ChunkSize (default = 100MB)\n\t\tbufferSize = min(int(fileSize), int(d.ChunkSize)*1024*1024)\n\t} else if fileSize > 10*1024*1024 {\n\t\t// Medium, use full file size for concurrent access\n\t\tbufferSize = int(fileSize)\n\t}\n\n\t// Create stream section reader for efficient chunking\n\tss, err := stream.NewStreamSectionReader(file, bufferSize, &up)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Cal minimal parallel upload threads, allows MediaFire resumable upload to rule it over custom value\n\t// If file is big, likely will respect d.UploadThreads instead of MediaFire's suggestion i.e. 5 threads\n\tthread := min(numUnits, d.UploadThreads)\n\n\t// Create ordered group for sequential upload processing with retry logic\n\tthreadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\n\tvar finalUploadKey string\n\tvar keyMutex sync.Mutex\n\n\tfileSize = file.GetSize()\n\tfor unitID := range numUnits {\n\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\tbreak\n\t\t}\n\n\t\tstart := int64(unitID) * unitSize\n\t\tsize := unitSize\n\t\tif start+size > fileSize {\n\t\t\tsize = fileSize - start\n\t\t}\n\n\t\tvar reader io.ReadSeeker\n\t\tvar unitHash string\n\n\t\t// Use lifecycle pattern for proper resource management\n\t\tthreadG.GoWithLifecycle(errgroup.Lifecycle{\n\t\t\tBefore: func(ctx context.Context) (err error) {\n\t\t\t\t// Skip already uploaded units\n\t\t\t\tif d.isUnitUploaded(intWords, unitID) {\n\t\t\t\t\treturn ss.DiscardSection(start, size)\n\t\t\t\t}\n\t\t\t\treader, err = ss.GetSectionReader(start, size)\n\t\t\t\treturn\n\t\t\t},\n\t\t\tDo: func(ctx context.Context) (err error) {\n\t\t\t\tif reader == nil {\n\t\t\t\t\treturn nil // Skip if reader is not initialized (already uploaded)\n\t\t\t\t}\n\t\t\t\treader.Seek(0, io.SeekStart)\n\n\t\t\t\tif unitHash == \"\" {\n\t\t\t\t\tvar err error\n\t\t\t\t\tunitHash, err = utils.HashReader(utils.SHA256, reader)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\treader.Seek(0, io.SeekStart)\n\t\t\t\t}\n\n\t\t\t\t// Perform upload\n\n\t\t\t\tactionToken, err := d.getActionToken(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif d.limiter != nil {\n\t\t\t\t\tif err := d.limiter.Wait(ctx); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"rate limit wait failed: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\turl := d.apiBase + \"/upload/resumable.php\"\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, driver.NewLimitedUploadStream(ctx, reader))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tq := req.URL.Query()\n\t\t\t\tq.Add(\"folder_key\", folderKey)\n\t\t\t\tq.Add(\"response_format\", \"json\")\n\t\t\t\tq.Add(\"session_token\", actionToken)\n\t\t\t\tq.Add(\"key\", uploadKey)\n\t\t\t\treq.URL.RawQuery = q.Encode()\n\n\t\t\t\treq.Header.Set(\"x-filehash\", fileHash)\n\t\t\t\treq.Header.Set(\"x-filesize\", strconv.FormatInt(fileSize, 10))\n\t\t\t\treq.Header.Set(\"x-unit-id\", strconv.Itoa(unitID))\n\t\t\t\treq.Header.Set(\"x-unit-size\", strconv.FormatInt(size, 10))\n\t\t\t\treq.Header.Set(\"x-unit-hash\", unitHash)\n\t\t\t\treq.Header.Set(\"x-filename\", filename)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t\t\t\treq.ContentLength = size\n\n\t\t\t\t/* fmt.Printf(\"Debug resumable upload request:\\n\")\n\t\t\t\tfmt.Printf(\"  URL: %s\\n\", req.URL.String())\n\t\t\t\tfmt.Printf(\"  Headers: %+v\\n\", req.Header)\n\t\t\t\tfmt.Printf(\"  Unit ID: %d\\n\", unitID)\n\t\t\t\tfmt.Printf(\"  Unit Size: %d\\n\", len(unitData))\n\t\t\t\tfmt.Printf(\"  Upload Key: %s\\n\", uploadKey)\n\t\t\t\tfmt.Printf(\"  Action Token: %s\\n\", actionToken) */\n\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read response body: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// fmt.Printf(\"MediaFire resumable upload response (status %d): %s\\n\", res.StatusCode, string(body))\n\n\t\t\t\tvar uploadResp struct {\n\t\t\t\t\tResponse struct {\n\t\t\t\t\t\tDoupload struct {\n\t\t\t\t\t\t\tKey string `json:\"key\"`\n\t\t\t\t\t\t} `json:\"doupload\"`\n\t\t\t\t\t\tResult string `json:\"result\"`\n\t\t\t\t\t} `json:\"response\"`\n\t\t\t\t}\n\n\t\t\t\tif err := json.Unmarshal(body, &uploadResp); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to parse response: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif res.StatusCode != 200 {\n\t\t\t\t\treturn fmt.Errorf(\"resumable upload failed with status %d\", res.StatusCode)\n\t\t\t\t}\n\n\t\t\t\t// Thread-safe update of final upload key\n\t\t\t\tkeyMutex.Lock()\n\t\t\t\tfinalUploadKey = uploadResp.Response.Doupload.Key\n\t\t\t\tkeyMutex.Unlock()\n\n\t\t\t\tup(float64(threadG.Success()+1) * 100 / float64(numUnits+1))\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tAfter: func(err error) {\n\t\t\t\tif reader != nil {\n\t\t\t\t\t// Cleanup resources\n\t\t\t\t\tss.FreeSectionReader(reader)\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\t}\n\n\tif err := threadG.Wait(); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn finalUploadKey, nil\n}\n\n/*func (d *Mediafire) uploadSingleUnit(ctx context.Context, file model.FileStreamer, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string, fileSize int64) (string, error) {\n\tstart := int64(unitID) * unitSize\n\tsize := unitSize\n\n\tif start+size > fileSize {\n\t\tsize = fileSize - start\n\t}\n\n\tunitData := make([]byte, size)\n\t_, err := file.Read(unitData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn d.resumableUpload(ctx, folderKey, uploadKey, unitData, unitID, fileHash, filename, fileSize)\n}*/\n\nfunc (d *Mediafire) getActionToken(ctx context.Context) (string, error) {\n\tif d.actionToken != \"\" {\n\t\treturn d.actionToken, nil\n\t}\n\n\tdata := map[string]string{\n\t\t\"type\":            \"upload\",\n\t\t\"lifespan\":        \"1440\",\n\t\t\"response_format\": \"json\",\n\t\t\"session_token\":   d.SessionToken,\n\t}\n\n\tvar resp MediafireActionTokenResponse\n\t_, err := d.postForm(ctx, \"/user/get_action_token.php\", data, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif resp.Response.Result != \"Success\" {\n\t\treturn \"\", fmt.Errorf(\"MediaFire action token failed: %s\", resp.Response.Result)\n\t}\n\n\treturn resp.Response.ActionToken, nil\n}\n\nfunc (d *Mediafire) pollUpload(ctx context.Context, key string) (*MediafirePollResponse, error) {\n\tactionToken, err := d.getActionToken(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get action token: %w\", err)\n\t}\n\n\t// fmt.Printf(\"Debug Key: %+v\\n\", key)\n\n\tquery := map[string]string{\n\t\t\"key\":             key,\n\t\t\"response_format\": \"json\",\n\t\t\"session_token\":   actionToken, /* d.SessionToken */\n\t}\n\n\tvar resp MediafirePollResponse\n\t_, err = d.postForm(ctx, \"/upload/poll_upload.php\", query, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// fmt.Printf(\"pollUpload :: Raw response: %s\\n\", string(body))\n\t// fmt.Printf(\"pollUpload :: Parsed response: %+v\\n\", resp)\n\n\t// fmt.Printf(\"pollUpload :: Debug Result: %+v\\n\", resp.Response.Result)\n\n\tif err := checkAPIResult(resp.Response.Result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Mediafire) isUnitUploaded(words []int, unitID int) bool {\n\twordIndex := unitID / 16\n\tbitIndex := unitID % 16\n\tif wordIndex >= len(words) {\n\t\treturn false\n\t}\n\treturn (words[wordIndex]>>bitIndex)&1 == 1\n}\n\nfunc (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, folderKey string) (*model.ObjThumb, error) {\n\t// First try to find by hash directly (most efficient)\n\tif fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {\n\t\treturn fileInfo, nil\n\t}\n\n\t// If hash search fails, search in the target folder\n\t// This is a fallback method in case the file exists but hash search doesn't work\n\tfiles, err := d.getFiles(ctx, folderKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, file := range files {\n\t\tif file.Name == filename && !file.IsFolder {\n\t\t\treturn d.fileToObj(file), nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"existing file not found\")\n}\n\nfunc (d *Mediafire) getFileByHash(ctx context.Context, hash string) (*model.ObjThumb, error) {\n\tquery := map[string]string{\n\t\t\"session_token\":   d.SessionToken,\n\t\t\"response_format\": \"json\",\n\t\t\"hash\":            hash,\n\t}\n\n\tvar resp MediafireFileSearchResponse\n\t_, err := d.postForm(ctx, \"/file/get_info.php\", query, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.Response.Result != \"Success\" {\n\t\treturn nil, fmt.Errorf(\"MediaFire file search failed: %s\", resp.Response.Result)\n\t}\n\n\tif len(resp.Response.FileInfo) == 0 {\n\t\treturn nil, fmt.Errorf(\"file not found by hash\")\n\t}\n\n\tfile := resp.Response.FileInfo[0]\n\treturn d.fileToObj(file), nil\n}\n\n"
  },
  {
    "path": "drivers/mediatrack/driver.go",
    "content": "package mediatrack\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype MediaTrack struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *MediaTrack) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *MediaTrack) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *MediaTrack) Init(ctx context.Context) error {\n\t_, err := d.request(\"https://kayle.api.mediatrack.cn/users\", http.MethodGet, nil, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *MediaTrack) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(f File) (model.Obj, error) {\n\t\tsize, _ := strconv.ParseInt(f.Size, 10, 64)\n\t\tthumb := \"\"\n\t\tif f.File != nil && f.File.Cover != \"\" {\n\t\t\tthumb = \"https://nano.mtres.cn/\" + f.File.Cover\n\t\t}\n\t\treturn &Object{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       f.ID,\n\t\t\t\tName:     f.Title,\n\t\t\t\tModified: f.UpdatedAt,\n\t\t\t\tIsFolder: f.File == nil,\n\t\t\t\tSize:     size,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: thumb},\n\t\t\tParentID:  dir.GetID(),\n\t\t}, nil\n\t})\n}\n\nfunc (d *MediaTrack) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\turl := fmt.Sprintf(\"https://kayn.api.mediatrack.cn/v1/download_token/asset?asset_id=%s&source_type=project&password=&source_id=%s\",\n\t\tfile.GetID(), d.ProjectID)\n\tlog.Debugf(\"media track url: %s\", url)\n\tbody, err := d.request(url, http.MethodGet, nil, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttoken := utils.Json.Get(body, \"data\", \"token\").ToString()\n\turl = \"https://kayn.api.mediatrack.cn/v1/download/redirect?token=\" + token\n\tres, err := base.NoRedirectClient.R().Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debug(res.String())\n\tlink := model.Link{\n\t\tURL: url,\n\t}\n\tlog.Debugln(\"res code: \", res.StatusCode())\n\tif res.StatusCode() == 302 {\n\t\tlink.URL = res.Header().Get(\"location\")\n\t\texpired := time.Duration(60) * time.Second\n\t\tlink.Expiration = &expired\n\t}\n\treturn &link, nil\n}\n\nfunc (d *MediaTrack) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\turl := fmt.Sprintf(\"https://jayce.api.mediatrack.cn/v3/assets/%s/children\", parentDir.GetID())\n\t_, err := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"type\":  1,\n\t\t\t\"title\": dirName,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := base.Json{\n\t\t\"parent_id\": dstDir.GetID(),\n\t\t\"ids\":       []string{srcObj.GetID()},\n\t}\n\turl := \"https://jayce.api.mediatrack.cn/v4/assets/batch/move\"\n\t_, err := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\turl := \"https://jayce.api.mediatrack.cn/v3/assets/\" + srcObj.GetID()\n\tdata := base.Json{\n\t\t\"title\": newName,\n\t}\n\t_, err := d.request(url, http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := base.Json{\n\t\t\"parent_id\": dstDir.GetID(),\n\t\t\"ids\":       []string{srcObj.GetID()},\n\t}\n\turl := \"https://jayce.api.mediatrack.cn/v4/assets/batch/clone\"\n\t_, err := d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Remove(ctx context.Context, obj model.Obj) error {\n\tvar parentID string\n\tif o, ok := obj.(*Object); ok {\n\t\tparentID = o.ParentID\n\t} else {\n\t\treturn fmt.Errorf(\"obj is not local Object\")\n\t}\n\tdata := base.Json{\n\t\t\"origin_id\": parentID,\n\t\t\"ids\":       []string{obj.GetID()},\n\t}\n\turl := \"https://jayce.api.mediatrack.cn/v4/assets/batch/delete\"\n\t_, err := d.request(url, http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tsrc := \"assets/\" + uuid.New().String()\n\tvar resp UploadResp\n\t_, err := d.request(\"https://jayce.api.mediatrack.cn/v3/storage/tokens/asset\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"src\", src)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcredential := resp.Data.Credentials\n\tcfg := &aws.Config{\n\t\tCredentials: credentials.NewStaticCredentials(credential.TmpSecretID, credential.TmpSecretKey, credential.Token),\n\t\tRegion:      &resp.Data.Region,\n\t\tEndpoint:    aws.String(\"cos.accelerate.myqcloud.com\"),\n\t}\n\ts, err := session.NewSession(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttempFile, err := file.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuploader := s3manager.NewUploader(s)\n\tif file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\tuploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)\n\t}\n\tinput := &s3manager.UploadInput{\n\t\tBucket: &resp.Data.Bucket,\n\t\tKey:    &resp.Data.Object,\n\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\t\tReader: tempFile,\n\t\t\t\tSize:   file.GetSize(),\n\t\t\t},\n\t\t\tUpdateProgress: up,\n\t\t}),\n\t}\n\t_, err = uploader.UploadWithContext(ctx, input)\n\tif err != nil {\n\t\treturn err\n\t}\n\turl := fmt.Sprintf(\"https://jayce.api.mediatrack.cn/v3/assets/%s/children\", dstDir.GetID())\n\t_, err = tempFile.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\th := md5.New()\n\t_, err = utils.CopyWithBuffer(h, tempFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\thash := hex.EncodeToString(h.Sum(nil))\n\tdata := base.Json{\n\t\t\"category\":    0,\n\t\t\"description\": file.GetName(),\n\t\t\"hash\":        hash,\n\t\t\"mime\":        file.GetMimetype(),\n\t\t\"size\":        file.GetSize(),\n\t\t\"src\":         src,\n\t\t\"title\":       file.GetName(),\n\t\t\"type\":        0,\n\t}\n\t_, err = d.request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nvar _ driver.Driver = (*MediaTrack)(nil)\n"
  },
  {
    "path": "drivers/mediatrack/meta.go",
    "content": "package mediatrack\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tAccessToken string `json:\"access_token\" required:\"true\"`\n\tProjectID   string `json:\"project_id\"`\n\tdriver.RootID\n\tOrderBy   string `json:\"order_by\" type:\"select\" options:\"updated_at,title,size\" default:\"title\"`\n\tOrderDesc bool   `json:\"order_desc\"`\n}\n\nvar config = driver.Config{\n\tName: \"MediaTrack\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &MediaTrack{}\n\t})\n}\n"
  },
  {
    "path": "drivers/mediatrack/types.go",
    "content": "package mediatrack\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype BaseResp struct {\n\tStatus  string `json:\"status\"`\n\tMessage string `json:\"message\"`\n}\ntype File struct {\n\tCategory     int           `json:\"category\"`\n\tChildAssets  []interface{} `json:\"childAssets\"`\n\tCommentCount int           `json:\"comment_count\"`\n\tCoverAsset   interface{}   `json:\"cover_asset\"`\n\tCoverAssetID string        `json:\"cover_asset_id\"`\n\tCreatedAt    time.Time     `json:\"created_at\"`\n\tDeletedAt    string        `json:\"deleted_at\"`\n\tDescription  string        `json:\"description\"`\n\tFile         *struct {\n\t\tCover string `json:\"cover\"`\n\t\tSrc   string `json:\"src\"`\n\t} `json:\"file\"`\n\t//FileID string `json:\"file_id\"`\n\tID string `json:\"id\"`\n\n\tSize       string        `json:\"size\"`\n\tThumbnails []interface{} `json:\"thumbnails\"`\n\tTitle      string        `json:\"title\"`\n\tUpdatedAt  time.Time     `json:\"updated_at\"`\n}\n\ntype ChildrenResp struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tTotal  int    `json:\"total\"`\n\t\tAssets []File `json:\"assets\"`\n\t} `json:\"data\"`\n\tPath      string `json:\"path\"`\n\tTraceID   string `json:\"trace_id\"`\n\tRequestID string `json:\"requestId\"`\n}\n\ntype UploadResp struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tCredentials struct {\n\t\t\tTmpSecretID  string    `json:\"TmpSecretId\"`\n\t\t\tTmpSecretKey string    `json:\"TmpSecretKey\"`\n\t\t\tToken        string    `json:\"Token\"`\n\t\t\tExpiredTime  int       `json:\"ExpiredTime\"`\n\t\t\tExpiration   time.Time `json:\"Expiration\"`\n\t\t\tStartTime    int       `json:\"StartTime\"`\n\t\t} `json:\"credentials\"`\n\t\tObject string `json:\"object\"`\n\t\tBucket string `json:\"bucket\"`\n\t\tRegion string `json:\"region\"`\n\t\tURL    string `json:\"url\"`\n\t\tSize   string `json:\"size\"`\n\t} `json:\"data\"`\n\tPath      string `json:\"path\"`\n\tTraceID   string `json:\"trace_id\"`\n\tRequestID string `json:\"requestId\"`\n}\n\ntype Object struct {\n\tmodel.Object\n\tmodel.Thumbnail\n\tParentID string\n}\n"
  },
  {
    "path": "drivers/mediatrack/util.go",
    "content": "package mediatrack\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *MediaTrack) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tvar e BaseResp\n\treq.SetResult(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugln(res.String())\n\tif e.Status != \"SUCCESS\" {\n\t\treturn nil, errors.New(e.Message)\n\t}\n\tif resp != nil {\n\t\terr = utils.Json.Unmarshal(res.Body(), resp)\n\t}\n\treturn res.Body(), err\n}\n\nfunc (d *MediaTrack) getFiles(parentId string) ([]File, error) {\n\tfiles := make([]File, 0)\n\turl := fmt.Sprintf(\"https://jayce.api.mediatrack.cn/v4/assets/%s/children\", parentId)\n\tsort := \"\"\n\tif d.OrderBy != \"\" {\n\t\tif d.OrderDesc {\n\t\t\tsort = \"-\"\n\t\t}\n\t\tsort += d.OrderBy\n\t}\n\tpage := 1\n\tfor {\n\t\tvar resp ChildrenResp\n\t\t_, err := d.request(url, http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"page\": strconv.Itoa(page),\n\t\t\t\t\"size\": \"50\",\n\t\t\t\t\"sort\": sort,\n\t\t\t})\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(resp.Data.Assets) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t\tfiles = append(files, resp.Data.Assets...)\n\t}\n\treturn files, nil\n}\n"
  },
  {
    "path": "drivers/mega/driver.go",
    "content": "package mega\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/rclone/rclone/lib/readers\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/t3rm1n4l/go-mega\"\n)\n\ntype Mega struct {\n\tmodel.Storage\n\tAddition\n\tc *mega.Mega\n}\n\nfunc (d *Mega) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Mega) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Mega) Init(ctx context.Context) error {\n\tvar twoFACode = d.TwoFACode\n\td.c = mega.New()\n\tif d.TwoFASecret != \"\" {\n\t\tcode, err := totp.GenerateCode(d.TwoFASecret, time.Now())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"generate totp code failed: %w\", err)\n\t\t}\n\t\ttwoFACode = code\n\t}\n\treturn d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)\n}\n\nfunc (d *Mega) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif node, ok := dir.(*MegaNode); ok {\n\t\tnodes, err := d.c.FS.GetChildren(node.n)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfn := make(map[string]model.Obj)\n\t\tfor i := range nodes {\n\t\t\tn := nodes[i]\n\t\t\tif n.GetType() != mega.FILE && n.GetType() != mega.FOLDER {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := fn[n.GetName()]; !ok {\n\t\t\t\tfn[n.GetName()] = &MegaNode{n}\n\t\t\t} else if sameNameObj := fn[n.GetName()]; (&MegaNode{n}).ModTime().After(sameNameObj.ModTime()) {\n\t\t\t\tfn[n.GetName()] = &MegaNode{n}\n\t\t\t}\n\t\t}\n\t\tres := make([]model.Obj, 0)\n\t\tfor _, v := range fn {\n\t\t\tres = append(res, v)\n\t\t}\n\t\treturn res, nil\n\t}\n\tlog.Errorf(\"can't convert: %+v\", dir)\n\treturn nil, fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) GetRoot(ctx context.Context) (model.Obj, error) {\n\tn := d.c.FS.GetRoot()\n\tlog.Debugf(\"mega root: %+v\", *n)\n\treturn &MegaNode{n}, nil\n}\n\nfunc (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif node, ok := file.(*MegaNode); ok {\n\n\t\t//down, err := d.c.NewDownload(n.Node)\n\t\t//if err != nil {\n\t\t//\treturn nil, fmt.Errorf(\"open download file failed: %w\", err)\n\t\t//}\n\n\t\tsize := file.GetSize()\n\t\tresultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\tlength := httpRange.Length\n\t\t\tif httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size {\n\t\t\t\tlength = size - httpRange.Start\n\t\t\t}\n\t\t\tvar down *mega.Download\n\t\t\terr := utils.Retry(3, time.Second, func() (err error) {\n\t\t\t\tdown, err = d.c.NewDownload(node.n)\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"open download file failed: %w\", err)\n\t\t\t}\n\t\t\too := &openObject{\n\t\t\t\tctx:  ctx,\n\t\t\t\td:    down,\n\t\t\t\tskip: httpRange.Start,\n\t\t\t}\n\n\t\t\treturn readers.NewLimitedReadCloser(oo, length), nil\n\t\t}\n\t\treturn &model.Link{\n\t\t\tRangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),\n\t\t}, nil\n\t}\n\treturn nil, fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif parentNode, ok := parentDir.(*MegaNode); ok {\n\t\t_, err := d.c.CreateDir(dirName, parentNode.n)\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif srcNode, ok := srcObj.(*MegaNode); ok {\n\t\tif dstNode, ok := dstDir.(*MegaNode); ok {\n\t\t\treturn d.c.Move(srcNode.n, dstNode.n)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif srcNode, ok := srcObj.(*MegaNode); ok {\n\t\treturn d.c.Rename(srcNode.n, newName)\n\t}\n\treturn fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *Mega) Remove(ctx context.Context, obj model.Obj) error {\n\tif node, ok := obj.(*MegaNode); ok {\n\t\treturn d.c.Delete(node.n, !d.MoveToTrash)\n\t}\n\treturn fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif dstNode, ok := dstDir.(*MegaNode); ok {\n\t\tu, err := d.c.NewUpload(dstNode.n, stream.GetName(), stream.GetSize())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treader := driver.NewLimitedUploadStream(ctx, stream)\n\t\tfor id := 0; id < u.Chunks(); id++ {\n\t\t\tif utils.IsCanceled(ctx) {\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\t\t\t_, chkSize, err := u.ChunkLocation(id)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tchunk := make([]byte, chkSize)\n\t\t\tn, err := io.ReadFull(reader, chunk)\n\t\t\tif err != nil && err != io.EOF {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif n != len(chunk) {\n\t\t\t\treturn errors.New(\"chunk too short\")\n\t\t\t}\n\n\t\t\terr = u.UploadChunk(id, chunk)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tup(float64(id) * 100 / float64(u.Chunks()))\n\t\t}\n\n\t\t_, err = u.Finish()\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"unable to convert dir to mega n\")\n}\n\nfunc (d *Mega) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tquota, err := d.c.GetQuota()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: int64(quota.Mstrg),\n\t\t\tUsedSpace:  int64(quota.Cstrg),\n\t\t},\n\t}, nil\n}\n\n//func (d *Mega) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Mega)(nil)\n"
  },
  {
    "path": "drivers/mega/meta.go",
    "content": "package mega\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\t//driver.RootPath\n\t//driver.RootID\n\tEmail       string `json:\"email\" required:\"true\"`\n\tPassword    string `json:\"password\" required:\"true\"`\n\tTwoFACode   string `json:\"two_fa_code\" required:\"false\" help:\"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver\"`\n\tTwoFASecret string `json:\"two_fa_secret\" required:\"false\" help:\"2FA secret\"`\n\tMoveToTrash bool   `json:\"move_to_trash\" default:\"true\" help:\"move to trash when deleting files\"`\n}\n\nvar config = driver.Config{\n\tName:      \"Mega_nz\",\n\tLocalSort: true,\n\tOnlyProxy: true,\n\tNoLinkURL: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Mega{}\n\t})\n}\n"
  },
  {
    "path": "drivers/mega/types.go",
    "content": "package mega\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/t3rm1n4l/go-mega\"\n)\n\ntype MegaNode struct {\n\tn *mega.Node\n}\n\nfunc (m *MegaNode) GetSize() int64 {\n\treturn m.n.GetSize()\n}\n\nfunc (m *MegaNode) GetName() string {\n\treturn m.n.GetName()\n}\n\nfunc (m *MegaNode) CreateTime() time.Time {\n\treturn m.n.GetTimeStamp()\n}\n\nfunc (m *MegaNode) GetHash() utils.HashInfo {\n\t//Meganz use md5, but can't get the original file hash, due to it's encrypted in the cloud\n\treturn utils.HashInfo{}\n}\n\nfunc (m *MegaNode) ModTime() time.Time {\n\treturn m.n.GetTimeStamp()\n}\n\nfunc (m *MegaNode) IsDir() bool {\n\treturn m.n.GetType() == mega.FOLDER || m.n.GetType() == mega.ROOT\n}\n\nfunc (m *MegaNode) GetID() string {\n\treturn m.n.GetHash()\n}\n\nfunc (m *MegaNode) GetPath() string {\n\treturn \"\"\n}\n\nvar _ model.Obj = (*MegaNode)(nil)\n"
  },
  {
    "path": "drivers/mega/util.go",
    "content": "package mega\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/t3rm1n4l/go-mega\"\n)\n\n// do others that not defined in Driver interface\n// openObject represents a download in progress\ntype openObject struct {\n\tctx    context.Context\n\tmu     sync.Mutex\n\td      *mega.Download\n\tid     int\n\tskip   int64\n\tchunk  []byte\n\tclosed bool\n}\n\n// get the next chunk\nfunc (oo *openObject) getChunk(ctx context.Context) (err error) {\n\tif oo.id >= oo.d.Chunks() {\n\t\treturn io.EOF\n\t}\n\tvar chunk []byte\n\terr = utils.Retry(3, time.Second, func() (err error) {\n\t\tchunk, err = oo.d.DownloadChunk(oo.id)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\too.id++\n\too.chunk = chunk\n\treturn nil\n}\n\n// Read reads up to len(p) bytes into p.\nfunc (oo *openObject) Read(p []byte) (n int, err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn 0, fmt.Errorf(\"read on closed file\")\n\t}\n\t// Skip data at the start if requested\n\tfor oo.skip > 0 {\n\t\t_, size, err := oo.d.ChunkLocation(oo.id)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip < int64(size) {\n\t\t\tbreak\n\t\t}\n\t\too.id++\n\t\too.skip -= int64(size)\n\t}\n\tif len(oo.chunk) == 0 {\n\t\terr = oo.getChunk(oo.ctx)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip > 0 {\n\t\t\too.chunk = oo.chunk[oo.skip:]\n\t\t\too.skip = 0\n\t\t}\n\t}\n\tn = copy(p, oo.chunk)\n\too.chunk = oo.chunk[n:]\n\treturn n, nil\n}\n\n// Close closed the file - MAC errors are reported here\nfunc (oo *openObject) Close() (err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn nil\n\t}\n\terr = utils.Retry(3, 500*time.Millisecond, func() (err error) {\n\t\treturn oo.d.Finish()\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to finish download: %w\", err)\n\t}\n\too.closed = true\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/misskey/driver.go",
    "content": "package misskey\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Misskey struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Misskey) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Misskey) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Misskey) Init(ctx context.Context) error {\n\td.Endpoint = strings.TrimSuffix(d.Endpoint, \"/\")\n\tif d.Endpoint == \"\" || d.AccessToken == \"\" {\n\t\treturn errs.EmptyToken\n\t} else {\n\t\treturn nil\n\t}\n}\n\nfunc (d *Misskey) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Misskey) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.list(dir)\n}\n\nfunc (d *Misskey) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\treturn d.link(file)\n}\n\nfunc (d *Misskey) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\treturn d.makeDir(parentDir, dirName)\n}\n\nfunc (d *Misskey) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.move(srcObj, dstDir)\n}\n\nfunc (d *Misskey) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\treturn d.rename(srcObj, newName)\n}\n\nfunc (d *Misskey) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.copy(srcObj, dstDir)\n}\n\nfunc (d *Misskey) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.remove(obj)\n}\n\nfunc (d *Misskey) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn d.put(ctx, dstDir, stream, up)\n}\n\n//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Misskey)(nil)\n"
  },
  {
    "path": "drivers/misskey/meta.go",
    "content": "package misskey\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// define other\n\t// Field string `json:\"field\" type:\"select\" required:\"true\" options:\"a,b,c\" default:\"a\"`\n\tEndpoint    string `json:\"endpoint\" required:\"true\" default:\"https://misskey.io\"`\n\tAccessToken string `json:\"access_token\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Misskey\",\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Misskey{}\n\t})\n}\n"
  },
  {
    "path": "drivers/misskey/types.go",
    "content": "package misskey\n\ntype Resp struct {\n\tCode int\n\tRaw  []byte\n}\n\ntype Properties struct {\n\tWidth  int `json:\"width\"`\n\tHeight int `json:\"height\"`\n}\n\ntype MFile struct {\n\tID           string     `json:\"id\"`\n\tCreatedAt    string     `json:\"createdAt\"`\n\tName         string     `json:\"name\"`\n\tType         string     `json:\"type\"`\n\tMD5          string     `json:\"md5\"`\n\tSize         int64      `json:\"size\"`\n\tIsSensitive  bool       `json:\"isSensitive\"`\n\tBlurhash     string     `json:\"blurhash\"`\n\tProperties   Properties `json:\"properties\"`\n\tURL          string     `json:\"url\"`\n\tThumbnailURL string     `json:\"thumbnailUrl\"`\n\tComment      *string    `json:\"comment\"`\n\tFolderID     *string    `json:\"folderId\"`\n\tFolder       MFolder    `json:\"folder\"`\n}\n\ntype MFolder struct {\n\tID        string  `json:\"id\"`\n\tCreatedAt string  `json:\"createdAt\"`\n\tName      string  `json:\"name\"`\n\tParentID  *string `json:\"parentId\"`\n}\n"
  },
  {
    "path": "drivers/misskey/util.go",
    "content": "package misskey\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// Base layer methods\n\nfunc (d *Misskey) request(path, method string, callback base.ReqCallback, resp interface{}) error {\n\turl := d.Endpoint + \"/api/drive\" + path\n\treq := base.RestyClient.R()\n\n\treq.SetAuthToken(d.AccessToken).SetHeader(\"Content-Type\", \"application/json\")\n\n\tif callback != nil {\n\t\tcallback(req)\n\t} else {\n\t\treq.SetBody(\"{}\")\n\t}\n\n\treq.SetResult(resp)\n\n\t// 启用调试模式\n\treq.EnableTrace()\n\n\tresponse, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !response.IsSuccess() {\n\t\treturn errors.New(response.String())\n\t}\n\treturn nil\n}\n\nfunc (d *Misskey) getThumb(ctx context.Context, obj model.Obj) (io.Reader, error) {\n\t// TODO return the thumb of obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc setBody(body interface{}) base.ReqCallback {\n\treturn func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}\n}\n\nfunc handleFolderId(dir model.Obj) interface{} {\n\tif isRootFolder(dir) {\n\t\treturn nil // Root folder doesn't need folderId\n\t}\n\treturn dir.GetID()\n}\n\nfunc isRootFolder(dir model.Obj) bool {\n\treturn dir.GetID() == \"\"\n}\n\n// API layer methods\n\nfunc (d *Misskey) getFiles(dir model.Obj) ([]model.Obj, error) {\n\tvar files []MFile\n\tvar body map[string]string\n\tif !isRootFolder(dir) {\n\t\tbody = map[string]string{\"folderId\": dir.GetID()}\n\t} else {\n\t\tbody = map[string]string{}\n\t}\n\terr := d.request(\"/files\", http.MethodPost, setBody(body), &files)\n\tif err != nil {\n\t\treturn []model.Obj{}, err\n\t}\n\treturn utils.SliceConvert(files, func(src MFile) (model.Obj, error) {\n\t\treturn mFile2Object(src), nil\n\t})\n}\n\nfunc (d *Misskey) getFolders(dir model.Obj) ([]model.Obj, error) {\n\tvar folders []MFolder\n\tvar body map[string]string\n\tif !isRootFolder(dir) {\n\t\tbody = map[string]string{\"folderId\": dir.GetID()}\n\t} else {\n\t\tbody = map[string]string{}\n\t}\n\terr := d.request(\"/folders\", http.MethodPost, setBody(body), &folders)\n\tif err != nil {\n\t\treturn []model.Obj{}, err\n\t}\n\treturn utils.SliceConvert(folders, func(src MFolder) (model.Obj, error) {\n\t\treturn mFolder2Object(src), nil\n\t})\n}\n\nfunc (d *Misskey) list(dir model.Obj) ([]model.Obj, error) {\n\tfiles, _ := d.getFiles(dir)\n\tfolders, _ := d.getFolders(dir)\n\treturn append(files, folders...), nil\n}\n\nfunc (d *Misskey) link(file model.Obj) (*model.Link, error) {\n\tvar mFile MFile\n\terr := d.request(\"/files/show\", http.MethodPost, setBody(map[string]string{\"fileId\": file.GetID()}), &mFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: mFile.URL,\n\t}, nil\n}\n\nfunc (d *Misskey) makeDir(parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar folder MFolder\n\terr := d.request(\"/folders/create\", http.MethodPost, setBody(map[string]interface{}{\"parentId\": handleFolderId(parentDir), \"name\": dirName}), &folder)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mFolder2Object(folder), nil\n}\n\nfunc (d *Misskey) move(srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\tvar folder MFolder\n\t\terr := d.request(\"/folders/update\", http.MethodPost, setBody(map[string]interface{}{\"folderId\": srcObj.GetID(), \"parentId\": handleFolderId(dstDir)}), &folder)\n\t\treturn mFolder2Object(folder), err\n\t} else {\n\t\tvar file MFile\n\t\terr := d.request(\"/files/update\", http.MethodPost, setBody(map[string]interface{}{\"fileId\": srcObj.GetID(), \"folderId\": handleFolderId(dstDir)}), &file)\n\t\treturn mFile2Object(file), err\n\t}\n}\n\nfunc (d *Misskey) rename(srcObj model.Obj, newName string) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\tvar folder MFolder\n\t\terr := d.request(\"/folders/update\", http.MethodPost, setBody(map[string]string{\"folderId\": srcObj.GetID(), \"name\": newName}), &folder)\n\t\treturn mFolder2Object(folder), err\n\t} else {\n\t\tvar file MFile\n\t\terr := d.request(\"/files/update\", http.MethodPost, setBody(map[string]string{\"fileId\": srcObj.GetID(), \"name\": newName}), &file)\n\t\treturn mFile2Object(file), err\n\t}\n}\n\nfunc (d *Misskey) copy(srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\tfolder, err := d.makeDir(dstDir, srcObj.GetName())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist, err := d.list(srcObj)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, obj := range list {\n\t\t\t_, err := d.copy(obj, folder)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn folder, nil\n\t} else {\n\t\tvar file MFile\n\t\turl, err := d.link(srcObj)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = d.request(\"/files/upload-from-url\", http.MethodPost, setBody(map[string]interface{}{\"url\": url.URL, \"folderId\": handleFolderId(dstDir)}), &file)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn mFile2Object(file), nil\n\t}\n}\n\nfunc (d *Misskey) remove(obj model.Obj) error {\n\tif obj.IsDir() {\n\t\terr := d.request(\"/folders/delete\", http.MethodPost, setBody(map[string]string{\"folderId\": obj.GetID()}), nil)\n\t\treturn err\n\t} else {\n\t\terr := d.request(\"/files/delete\", http.MethodPost, setBody(map[string]string{\"fileId\": obj.GetID()}), nil)\n\t\treturn err\n\t}\n}\n\nfunc (d *Misskey) put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tvar file MFile\n\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         stream,\n\t\tUpdateProgress: up,\n\t})\n\n\t// Build form data, only add folderId if not root folder\n\tformData := map[string]string{\n\t\t\"name\":        stream.GetName(),\n\t\t\"comment\":     \"\",\n\t\t\"isSensitive\": \"false\",\n\t\t\"force\":       \"false\",\n\t}\n\n\tfolderId := handleFolderId(dstDir)\n\tif folderId != nil {\n\t\tformData[\"folderId\"] = folderId.(string)\n\t}\n\n\treq := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetFileReader(\"file\", stream.GetName(), reader).\n\t\tSetFormData(formData).\n\t\tSetResult(&file).\n\t\tSetAuthToken(d.AccessToken)\n\n\tresp, err := req.Post(d.Endpoint + \"/api/drive/files/create\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !resp.IsSuccess() {\n\t\treturn nil, errors.New(resp.String())\n\t}\n\n\treturn mFile2Object(file), nil\n}\n\nfunc mFile2Object(file MFile) *model.ObjThumbURL {\n\tctime, err := time.Parse(time.RFC3339, file.CreatedAt)\n\tif err != nil {\n\t\tctime = time.Time{}\n\t}\n\treturn &model.ObjThumbURL{\n\t\tObject: model.Object{\n\t\t\tID:       file.ID,\n\t\t\tName:     file.Name,\n\t\t\tCtime:    ctime,\n\t\t\tIsFolder: false,\n\t\t\tSize:     file.Size,\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: file.ThumbnailURL,\n\t\t},\n\t\tUrl: model.Url{\n\t\t\tUrl: file.URL,\n\t\t},\n\t}\n}\n\nfunc mFolder2Object(folder MFolder) *model.Object {\n\tctime, err := time.Parse(time.RFC3339, folder.CreatedAt)\n\tif err != nil {\n\t\tctime = time.Time{}\n\t}\n\treturn &model.Object{\n\t\tID:       folder.ID,\n\t\tName:     folder.Name,\n\t\tCtime:    ctime,\n\t\tIsFolder: true,\n\t}\n}\n"
  },
  {
    "path": "drivers/mopan/driver.go",
    "content": "package mopan\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/foxxorcat/mopan-sdk-go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype MoPan struct {\n\tmodel.Storage\n\tAddition\n\tclient *mopan.MoClient\n\n\tuserID       string\n\tuploadThread int\n}\n\nfunc (d *MoPan) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *MoPan) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *MoPan) Init(ctx context.Context) error {\n\td.uploadThread, _ = strconv.Atoi(d.UploadThread)\n\tif d.uploadThread < 1 || d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 3, \"3\"\n\t}\n\n\tdefer func() { d.SMSCode = \"\" }()\n\n\tlogin := func() (err error) {\n\t\tvar loginData *mopan.LoginResp\n\t\tif d.SMSCode != \"\" {\n\t\t\tloginData, err = d.client.LoginBySmsStep2(d.Phone, d.SMSCode)\n\t\t} else {\n\t\t\tloginData, err = d.client.Login(d.Phone, d.Password)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.client.SetAuthorization(loginData.Token)\n\n\t\tinfo, err := d.client.GetUserInfo()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.userID = info.UserID\n\t\tlog.Debugf(\"[mopan] Phone: %s UserCloudStorageRelations: %+v\", d.Phone, loginData.UserCloudStorageRelations)\n\t\tcloudCircleApp, _ := d.client.QueryAllCloudCircleApp()\n\t\tlog.Debugf(\"[mopan] Phone: %s CloudCircleApp: %+v\", d.Phone, cloudCircleApp)\n\t\tif d.RootFolderID == \"\" {\n\t\t\tfor _, userCloudStorage := range loginData.UserCloudStorageRelations {\n\t\t\t\tif userCloudStorage.Path == \"/文件\" {\n\t\t\t\t\td.RootFolderID = userCloudStorage.FolderID\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\td.client = mopan.NewMoClientWithRestyClient(base.NewRestyClient()).\n\t\tSetRestyClient(base.RestyClient).\n\t\tSetOnAuthorizationExpired(func(_ error) error {\n\t\t\terr := login()\n\t\t\tif err != nil {\n\t\t\t\td.Status = err.Error()\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\n\tvar deviceInfo mopan.DeviceInfo\n\tif strings.TrimSpace(d.DeviceInfo) != \"\" && utils.Json.UnmarshalFromString(d.DeviceInfo, &deviceInfo) == nil {\n\t\td.client.SetDeviceInfo(&deviceInfo)\n\t}\n\td.DeviceInfo, _ = utils.Json.MarshalToString(d.client.GetDeviceInfo())\n\n\tif strings.Contains(d.SMSCode, \"send\") {\n\t\tif _, err := d.client.LoginBySms(d.Phone); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn errors.New(\"please enter the SMS code\")\n\t}\n\treturn login()\n}\n\nfunc (d *MoPan) Drop(ctx context.Context) error {\n\td.client = nil\n\td.userID = \"\"\n\treturn nil\n}\n\nfunc (d *MoPan) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar files []model.Obj\n\tfor page := 1; ; page++ {\n\t\tdata, err := d.client.QueryFiles(dir.GetID(), page, mopan.WarpParamOption(\n\t\t\tfunc(j mopan.Json) {\n\t\t\t\tj[\"orderBy\"] = d.OrderBy\n\t\t\t\tj[\"descending\"] = d.OrderDirection == \"desc\"\n\t\t\t},\n\t\t\tmopan.ParamOptionShareFile(d.CloudID),\n\t\t))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(data.FileListAO.FileList)+len(data.FileListAO.FolderList) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tlog.Debugf(\"[mopan] Phone: %s folder: %+v\", d.Phone, data.FileListAO.FolderList)\n\t\tfiles = append(files, utils.MustSliceConvert(data.FileListAO.FolderList, folderToObj)...)\n\t\tfiles = append(files, utils.MustSliceConvert(data.FileListAO.FileList, fileToObj)...)\n\t}\n\treturn files, nil\n}\n\nfunc (d *MoPan) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdata, err := d.client.GetFileDownloadUrl(file.GetID(), mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata.DownloadUrl = strings.Replace(strings.ReplaceAll(data.DownloadUrl, \"&amp;\", \"&\"), \"http://\", \"https://\", 1)\n\tres, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Get(data.DownloadUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\t_ = res.RawBody().Close()\n\t}()\n\tif res.StatusCode() == 302 {\n\t\tdata.DownloadUrl = res.Header().Get(\"location\")\n\t}\n\n\treturn &model.Link{\n\t\tURL: data.DownloadUrl,\n\t}, nil\n}\n\nfunc (d *MoPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tf, err := d.client.CreateFolder(dirName, parentDir.GetID(), mopan.WarpParamOption(\n\t\tmopan.ParamOptionShareFile(d.CloudID),\n\t))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn folderToObj(*f), nil\n}\n\nfunc (d *MoPan) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.newTask(srcObj, dstDir, mopan.TASK_MOVE)\n}\n\nfunc (d *MoPan) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\t_, err := d.client.RenameFolder(srcObj.GetID(), newName, mopan.WarpParamOption(\n\t\t\tmopan.ParamOptionShareFile(d.CloudID),\n\t\t))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\t_, err := d.client.RenameFile(srcObj.GetID(), newName, mopan.WarpParamOption(\n\t\t\tmopan.ParamOptionShareFile(d.CloudID),\n\t\t))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn CloneObj(srcObj, srcObj.GetID(), newName), nil\n}\n\nfunc (d *MoPan) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.newTask(srcObj, dstDir, mopan.TASK_COPY)\n}\n\nfunc (d *MoPan) newTask(srcObj, dstDir model.Obj, taskType mopan.TaskType) (model.Obj, error) {\n\tparam := mopan.TaskParam{\n\t\tUserOrCloudID:       d.userID,\n\t\tSource:              1,\n\t\tTaskType:            taskType,\n\t\tTargetSource:        1,\n\t\tTargetUserOrCloudID: d.userID,\n\t\tTargetType:          1,\n\t\tTargetFolderID:      dstDir.GetID(),\n\t\tTaskStatusDetailDTOList: []mopan.TaskFileParam{\n\t\t\t{\n\t\t\t\tFileID:   srcObj.GetID(),\n\t\t\t\tIsFolder: srcObj.IsDir(),\n\t\t\t\tFileName: srcObj.GetName(),\n\t\t\t},\n\t\t},\n\t}\n\tif d.CloudID != \"\" {\n\t\tparam.UserOrCloudID = d.CloudID\n\t\tparam.Source = 2\n\t\tparam.TargetSource = 2\n\t\tparam.TargetUserOrCloudID = d.CloudID\n\t}\n\n\ttask, err := d.client.AddBatchTask(param)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor count := 0; count < 5; count++ {\n\t\tstat, err := d.client.CheckBatchTask(mopan.TaskCheckParam{\n\t\t\tTaskId:              task.TaskIDList[0],\n\t\t\tTaskType:            task.TaskType,\n\t\t\tTargetType:          1,\n\t\t\tTargetFolderID:      task.TargetFolderID,\n\t\t\tTargetSource:        param.TargetSource,\n\t\t\tTargetUserOrCloudID: param.TargetUserOrCloudID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch stat.TaskStatus {\n\t\tcase 2:\n\t\t\tif err := d.client.CancelBatchTask(stat.TaskID, task.TaskType); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn nil, errors.New(\"file name conflict\")\n\t\tcase 4:\n\t\t\tif task.TaskType == mopan.TASK_MOVE {\n\t\t\t\treturn CloneObj(srcObj, srcObj.GetID(), srcObj.GetName()), nil\n\t\t\t}\n\t\t\treturn CloneObj(srcObj, stat.SuccessedFileIDList[0], srcObj.GetName()), nil\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n\treturn nil, nil\n}\n\nfunc (d *MoPan) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := d.client.DeleteToRecycle([]mopan.TaskFileParam{\n\t\t{\n\t\t\tFileID:   obj.GetID(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t\tFileName: obj.GetName(),\n\t\t},\n\t}, mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID)))\n\treturn err\n}\n\nfunc (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tfile, err := stream.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// step.1\n\tuploadPartData, err := mopan.InitUploadPartData(ctx, mopan.UpdloadFileParam{\n\t\tParentFolderId: dstDir.GetID(),\n\t\tFileName:       stream.GetName(),\n\t\tFileSize:       stream.GetSize(),\n\t\tFile:           file,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 尝试恢复进度\n\tinitUpdload, ok := base.GetUploadProgress[*mopan.InitMultiUploadData](d, d.client.Authorization, uploadPartData.FileMd5)\n\tif !ok {\n\t\t// step.2\n\t\tinitUpdload, err = d.client.InitMultiUpload(ctx, *uploadPartData, mopan.WarpParamOption(\n\t\t\tmopan.ParamOptionShareFile(d.CloudID),\n\t\t))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif !initUpdload.FileDataExists {\n\t\t// utils.Log.Error(d.client.CloudDiskStartBusiness())\n\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,\n\t\t\tretry.Attempts(3),\n\t\t\tretry.Delay(time.Second),\n\t\t\tretry.DelayType(retry.BackOffDelay))\n\n\t\t// step.3\n\t\tparts, err := d.client.GetAllMultiUploadUrls(initUpdload.UploadFileID, initUpdload.PartInfos)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i, part := range parts {\n\t\t\tif utils.IsCanceled(upCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ti, part, byteSize := i, part, initUpdload.PartSize\n\t\t\tif part.PartNumber == uploadPartData.PartTotal {\n\t\t\t\tbyteSize = initUpdload.LastPartSize\n\t\t\t}\n\n\t\t\t// step.4\n\t\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t\treader := io.NewSectionReader(file, int64(part.PartNumber-1)*initUpdload.PartSize, byteSize)\n\t\t\t\treq, err := part.NewRequest(ctx, driver.NewLimitedUploadStream(ctx, reader))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\tresp, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t_ = resp.Body.Close()\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn fmt.Errorf(\"upload err,code=%d\", resp.StatusCode)\n\t\t\t\t}\n\t\t\t\tup(100 * float64(threadG.Success()+1) / float64(len(parts)+1))\n\t\t\t\tinitUpdload.PartInfos[i] = \"\"\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\tif err = threadG.Wait(); err != nil {\n\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\tinitUpdload.PartInfos = utils.SliceFilter(initUpdload.PartInfos, func(s string) bool { return s != \"\" })\n\t\t\t\tbase.SaveUploadProgress(d, initUpdload, d.client.Authorization, uploadPartData.FileMd5)\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t//step.5\n\tuFile, err := d.client.CommitMultiUploadFile(initUpdload.UploadFileID, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{\n\t\tID:       uFile.UserFileID,\n\t\tName:     uFile.FileName,\n\t\tSize:     int64(uFile.FileSize),\n\t\tModified: time.Time(uFile.CreateDate),\n\t}, nil\n}\n\nfunc (d *MoPan) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tquota, err := d.client.UsedSpace()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: int64(quota.Capacity),\n\t\t\tUsedSpace:  int64(quota.Used),\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*MoPan)(nil)\nvar _ driver.MkdirResult = (*MoPan)(nil)\nvar _ driver.MoveResult = (*MoPan)(nil)\nvar _ driver.RenameResult = (*MoPan)(nil)\nvar _ driver.Remove = (*MoPan)(nil)\nvar _ driver.CopyResult = (*MoPan)(nil)\nvar _ driver.PutResult = (*MoPan)(nil)\n"
  },
  {
    "path": "drivers/mopan/meta.go",
    "content": "package mopan\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tPhone    string `json:\"phone\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\tSMSCode  string `json:\"sms_code\" help:\"input 'send' send sms \"`\n\n\tRootFolderID string `json:\"root_folder_id\" default:\"\"`\n\n\tCloudID string `json:\"cloud_id\"`\n\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"filename,filesize,lastOpTime\" default:\"filename\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\n\tDeviceInfo string `json:\"device_info\"`\n\n\tUploadThread string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\n}\n\nfunc (a *Addition) GetRootId() string {\n\treturn a.RootFolderID\n}\n\nvar config = driver.Config{\n\tName:        \"MoPan\",\n\tCheckStatus: true,\n\tAlert:       \"warning|This network disk may store your password in clear text. Please set your password carefully\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &MoPan{}\n\t})\n}\n"
  },
  {
    "path": "drivers/mopan/types.go",
    "content": "package mopan\n"
  },
  {
    "path": "drivers/mopan/util.go",
    "content": "package mopan\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/foxxorcat/mopan-sdk-go\"\n)\n\nfunc fileToObj(f mopan.File) model.Obj {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       string(f.ID),\n\t\t\tName:     f.Name,\n\t\t\tSize:     int64(f.Size),\n\t\t\tModified: time.Time(f.LastOpTime),\n\t\t\tCtime:    time.Time(f.CreateDate),\n\t\t\tHashInfo: utils.NewHashInfo(utils.MD5, f.Md5),\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: f.Icon.SmallURL,\n\t\t},\n\t}\n}\n\nfunc folderToObj(f mopan.Folder) model.Obj {\n\treturn &model.Object{\n\t\tID:       string(f.ID),\n\t\tName:     f.Name,\n\t\tModified: time.Time(f.LastOpTime),\n\t\tCtime:    time.Time(f.CreateDate),\n\t\tIsFolder: true,\n\t}\n}\n\nfunc CloneObj(o model.Obj, newID, newName string) model.Obj {\n\tif o.IsDir() {\n\t\treturn &model.Object{\n\t\t\tID:       newID,\n\t\t\tName:     newName,\n\t\t\tIsFolder: true,\n\t\t\tModified: o.ModTime(),\n\t\t\tCtime:    o.CreateTime(),\n\t\t}\n\t}\n\n\tthumb := \"\"\n\tif o, ok := o.(model.Thumb); ok {\n\t\tthumb = o.Thumb()\n\t}\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       newID,\n\t\t\tName:     newName,\n\t\t\tSize:     o.GetSize(),\n\t\t\tModified: o.ModTime(),\n\t\t\tCtime:    o.CreateTime(),\n\t\t\tHashInfo: o.GetHash(),\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: thumb,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "drivers/netease_music/crypto.go",
    "content": "package netease_music\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/md5\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n)\n\nvar (\n\tlinuxapiKey = []byte(\"rFgB&h#%2?^eDg:Q\")\n\teapiKey     = []byte(\"e82ckenh8dichen8\")\n\tiv          = []byte(\"0102030405060708\")\n\tpresetKey   = []byte(\"0CoJUm6Qyw8W8jud\")\n\tpublicKey   = []byte(\"-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\\n-----END PUBLIC KEY-----\")\n\tstdChars    = []byte(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n)\n\nfunc aesKeyPending(key []byte) []byte {\n\tk := len(key)\n\tcount := 0\n\tswitch true {\n\tcase k <= 16:\n\t\tcount = 16 - k\n\tcase k <= 24:\n\t\tcount = 24 - k\n\tcase k <= 32:\n\t\tcount = 32 - k\n\tdefault:\n\t\treturn key[:32]\n\t}\n\tif count == 0 {\n\t\treturn key\n\t}\n\n\treturn append(key, bytes.Repeat([]byte{0}, count)...)\n}\n\nfunc pkcs7Padding(src []byte, blockSize int) []byte {\n\tpadding := blockSize - len(src)%blockSize\n\tpadtext := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(src, padtext...)\n}\n\nfunc aesCBCEncrypt(src, key, iv []byte) []byte {\n\tblock, _ := aes.NewCipher(aesKeyPending(key))\n\tsrc = pkcs7Padding(src, block.BlockSize())\n\tdst := make([]byte, len(src))\n\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tmode.CryptBlocks(dst, src)\n\n\treturn dst\n}\n\nfunc aesECBEncrypt(src, key []byte) []byte {\n\tblock, _ := aes.NewCipher(aesKeyPending(key))\n\n\tsrc = pkcs7Padding(src, block.BlockSize())\n\tdst := make([]byte, len(src))\n\n\tecbCryptBlocks(block, dst, src)\n\n\treturn dst\n}\n\nfunc ecbCryptBlocks(block cipher.Block, dst, src []byte) {\n\tbs := block.BlockSize()\n\n\tfor len(src) > 0 {\n\t\tblock.Encrypt(dst, src[:bs])\n\t\tsrc = src[bs:]\n\t\tdst = dst[bs:]\n\t}\n}\n\nfunc rsaEncrypt(buffer, key []byte) []byte {\n\tbuffers := make([]byte, 128-16, 128)\n\tbuffers = append(buffers, buffer...)\n\tblock, _ := pem.Decode(key)\n\tpubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)\n\tpub := pubInterface.(*rsa.PublicKey)\n\tc := new(big.Int).SetBytes([]byte(buffers))\n\treturn c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes()\n}\n\nfunc getSecretKey() ([]byte, []byte) {\n\tkey := make([]byte, 16)\n\treversed := make([]byte, 16)\n\tfor i := 0; i < 16; i++ {\n\t\tresult := stdChars[random.RangeInt64(0, 62)]\n\t\tkey[i] = result\n\t\treversed[15-i] = result\n\t}\n\treturn key, reversed\n}\n\nfunc weapi(data map[string]string) map[string]string {\n\ttext, _ := utils.Json.Marshal(data)\n\tsecretKey, reversedKey := getSecretKey()\n\tparams := []byte(base64.StdEncoding.EncodeToString(aesCBCEncrypt(text, presetKey, iv)))\n\treturn map[string]string{\n\t\t\"params\":    base64.StdEncoding.EncodeToString(aesCBCEncrypt(params, reversedKey, iv)),\n\t\t\"encSecKey\": hex.EncodeToString(rsaEncrypt(secretKey, publicKey)),\n\t}\n}\n\nfunc eapi(url string, data map[string]interface{}) map[string]string {\n\ttext, _ := utils.Json.Marshal(data)\n\tmsg := \"nobody\" + url + \"use\" + string(text) + \"md5forencrypt\"\n\th := md5.New()\n\th.Write([]byte(msg))\n\tdigest := hex.EncodeToString(h.Sum(nil))\n\tparams := []byte(url + \"-36cd479b6b5-\" + string(text) + \"-36cd479b6b5-\" + digest)\n\treturn map[string]string{\n\t\t\"params\": hex.EncodeToString(aesECBEncrypt(params, eapiKey)),\n\t}\n}\n\nfunc linuxapi(data map[string]interface{}) map[string]string {\n\ttext, _ := utils.Json.Marshal(data)\n\treturn map[string]string{\n\t\t\"eparams\": strings.ToUpper(hex.EncodeToString(aesECBEncrypt(text, linuxapiKey))),\n\t}\n}\n"
  },
  {
    "path": "drivers/netease_music/driver.go",
    "content": "package netease_music\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t_ \"golang.org/x/image/webp\"\n)\n\ntype NeteaseMusic struct {\n\tmodel.Storage\n\tAddition\n\n\tcsrfToken     string\n\tmusicU        string\n\tfileMapByName map[string]model.Obj\n}\n\nfunc (d *NeteaseMusic) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *NeteaseMusic) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *NeteaseMusic) Init(ctx context.Context) error {\n\td.csrfToken = d.Addition.getCookie(\"__csrf\")\n\td.musicU = d.Addition.getCookie(\"MUSIC_U\")\n\n\tif d.csrfToken == \"\" || d.musicU == \"\" {\n\t\treturn errs.EmptyToken\n\t}\n\n\treturn nil\n}\n\nfunc (d *NeteaseMusic) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (Addition) GetRootPath() string {\n\treturn \"/\"\n}\n\nfunc (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {\n\tfragments := strings.Split(path, \"/\")\n\tif len(fragments) > 1 {\n\t\tfileName := fragments[1]\n\t\tif strings.HasSuffix(fileName, \".lrc\") {\n\t\t\tlrc := d.fileMapByName[fileName]\n\t\t\treturn d.getLyricObj(lrc)\n\t\t}\n\t\tif song, ok := d.fileMapByName[fileName]; ok {\n\t\t\treturn song, nil\n\t\t} else {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t}\n\n\treturn nil, errs.ObjectNotFound\n}\n\nfunc (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.getSongObjs(args)\n}\n\nfunc (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif lrc, ok := file.(*LyricObj); ok {\n\t\tif args.Type == \"parsed\" && !args.Redirect {\n\t\t\treturn lrc.getLyricLink(), nil\n\t\t} else {\n\t\t\treturn lrc.getProxyLink(ctx), nil\n\t\t}\n\t}\n\n\treturn d.getSongLink(file)\n}\n\nfunc (d *NeteaseMusic) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.removeSongObj(obj)\n}\n\nfunc (d *NeteaseMusic) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\treturn d.putSongStream(ctx, stream, up)\n}\n\nfunc (d *NeteaseMusic) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *NeteaseMusic) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *NeteaseMusic) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *NeteaseMusic) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn errs.NotSupport\n}\n\nvar _ driver.Driver = (*NeteaseMusic)(nil)\n"
  },
  {
    "path": "drivers/netease_music/meta.go",
    "content": "package netease_music\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tCookie    string `json:\"cookie\" type:\"text\" required:\"true\" help:\"\"`\n\tSongLimit uint64 `json:\"song_limit\" default:\"200\" type:\"number\" help:\"only get 200 songs by default\"`\n}\n\nfunc (ad *Addition) getCookie(name string) string {\n\tre := regexp.MustCompile(name + \"=([^(;|$)]+)\")\n\tmatches := re.FindStringSubmatch(ad.Cookie)\n\tif len(matches) < 2 {\n\t\treturn \"\"\n\t}\n\treturn matches[1]\n}\n\nvar config = driver.Config{\n\tName: \"NeteaseMusic\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &NeteaseMusic{}\n\t})\n}\n"
  },
  {
    "path": "drivers/netease_music/types.go",
    "content": "package netease_music\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\ntype HostsResp struct {\n\tUpload []string `json:\"upload\"`\n}\n\ntype SongResp struct {\n\tData []struct {\n\t\tUrl string `json:\"url\"`\n\t} `json:\"data\"`\n}\n\ntype ListResp struct {\n\tSize    int64 `json:\"size\"`\n\tMaxSize int64 `json:\"maxSize\"`\n\tData    []struct {\n\t\tAddTime    int64  `json:\"addTime\"`\n\t\tFileName   string `json:\"fileName\"`\n\t\tFileSize   int64  `json:\"fileSize\"`\n\t\tSongId     int64  `json:\"songId\"`\n\t\tSimpleSong struct {\n\t\t\tAl struct {\n\t\t\t\tPicUrl string `json:\"picUrl\"`\n\t\t\t} `json:\"al\"`\n\t\t} `json:\"simpleSong\"`\n\t} `json:\"data\"`\n}\n\ntype LyricObj struct {\n\tmodel.Object\n\tlyric string\n}\n\nfunc (lrc *LyricObj) getProxyLink(ctx context.Context) *model.Link {\n\trawURL := common.GetApiUrl(ctx) + \"/p\" + lrc.Path\n\trawURL = utils.EncodePath(rawURL, true) + \"?type=parsed&sign=\" + sign.Sign(lrc.Path)\n\treturn &model.Link{URL: rawURL}\n}\n\nfunc (lrc *LyricObj) getLyricLink() *model.Link {\n\treturn &model.Link{\n\t\tRangeReader: stream.GetRangeReaderFromMFile(int64(len(lrc.lyric)), strings.NewReader(lrc.lyric)),\n\t}\n}\n\ntype ReqOption struct {\n\tcrypto  string\n\tstream  model.FileStreamer\n\tup      driver.UpdateProgress\n\tctx     context.Context\n\tdata    map[string]string\n\theaders map[string]string\n\tcookies []*http.Cookie\n\turl     string\n}\n\ntype Characteristic map[string]string\n\nfunc (ch *Characteristic) fromDriver(d *NeteaseMusic) *Characteristic {\n\t*ch = map[string]string{\n\t\t\"osver\":       \"\",\n\t\t\"deviceId\":    \"\",\n\t\t\"mobilename\":  \"\",\n\t\t\"appver\":      \"6.1.1\",\n\t\t\"versioncode\": \"140\",\n\t\t\"buildver\":    strconv.FormatInt(time.Now().Unix(), 10),\n\t\t\"resolution\":  \"1920x1080\",\n\t\t\"os\":          \"android\",\n\t\t\"channel\":     \"\",\n\t\t\"requestId\":   strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(int(random.RangeInt64(0, 1000))),\n\t\t\"MUSIC_U\":     d.musicU,\n\t}\n\treturn ch\n}\n\nfunc (ch Characteristic) toCookies() []*http.Cookie {\n\tcookies := make([]*http.Cookie, 0)\n\tfor k, v := range ch {\n\t\tcookies = append(cookies, &http.Cookie{Name: k, Value: v})\n\t}\n\treturn cookies\n}\n\nfunc (ch *Characteristic) merge(data map[string]string) map[string]interface{} {\n\tbody := map[string]interface{}{\n\t\t\"header\": ch,\n\t}\n\tfor k, v := range data {\n\t\tbody[k] = v\n\t}\n\treturn body\n}\n"
  },
  {
    "path": "drivers/netease_music/upload.go",
    "content": "package netease_music\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/dhowden/tag\"\n)\n\ntype token struct {\n\tresourceId string\n\tobjectKey  string\n\ttoken      string\n}\n\ntype songmeta struct {\n\tneedUpload bool\n\tsongId     string\n\tname       string\n\tartist     string\n\talbum      string\n}\n\ntype uploader struct {\n\tdriver   *NeteaseMusic\n\tfile     model.File\n\tmeta     songmeta\n\tmd5      string\n\text      string\n\tsize     string\n\tfilename string\n}\n\nfunc (u *uploader) init(stream model.FileStreamer) error {\n\tu.filename = stream.GetName()\n\tu.size = strconv.FormatInt(stream.GetSize(), 10)\n\n\tu.ext = \"mp3\"\n\tif strings.HasSuffix(stream.GetMimetype(), \"flac\") {\n\t\tu.ext = \"flac\"\n\t}\n\n\th := md5.New()\n\t_, err := utils.CopyWithBuffer(h, stream)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu.md5 = hex.EncodeToString(h.Sum(nil))\n\t_, err = u.file.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif m, err := tag.ReadFrom(u.file); err != nil {\n\t\tu.meta = songmeta{}\n\t} else {\n\t\tu.meta = songmeta{\n\t\t\tname:   m.Title(),\n\t\t\tartist: m.Artist(),\n\t\t\talbum:  m.Album(),\n\t\t}\n\t}\n\tif u.meta.name == \"\" {\n\t\tu.meta.name = u.filename\n\t}\n\tif u.meta.album == \"\" {\n\t\tu.meta.album = \"未知专辑\"\n\t}\n\tif u.meta.artist == \"\" {\n\t\tu.meta.artist = \"未知艺术家\"\n\t}\n\t_, err = u.file.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (u *uploader) checkIfExisted() error {\n\tbody, err := u.driver.request(\"https://interface.music.163.com/api/cloud/upload/check\", http.MethodPost,\n\t\tReqOption{\n\t\t\tcrypto: \"weapi\",\n\t\t\tdata: map[string]string{\n\t\t\t\t\"ext\":     \"\",\n\t\t\t\t\"songId\":  \"0\",\n\t\t\t\t\"version\": \"1\",\n\t\t\t\t\"bitrate\": \"999000\",\n\t\t\t\t\"length\":  u.size,\n\t\t\t\t\"md5\":     u.md5,\n\t\t\t},\n\t\t\tcookies: []*http.Cookie{\n\t\t\t\t{Name: \"os\", Value: \"pc\"},\n\t\t\t\t{Name: \"appver\", Value: \"2.9.7\"},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu.meta.songId = utils.Json.Get(body, \"songId\").ToString()\n\tu.meta.needUpload = utils.Json.Get(body, \"needUpload\").ToBool()\n\n\treturn nil\n}\n\nfunc (u *uploader) allocToken(bucket ...string) (token, error) {\n\tif len(bucket) == 0 {\n\t\tbucket = []string{\"\"}\n\t}\n\n\tbody, err := u.driver.request(\"https://music.163.com/weapi/nos/token/alloc\", http.MethodPost, ReqOption{\n\t\tcrypto: \"weapi\",\n\t\tdata: map[string]string{\n\t\t\t\"bucket\":      bucket[0],\n\t\t\t\"local\":       \"false\",\n\t\t\t\"type\":        \"audio\",\n\t\t\t\"nos_product\": \"3\",\n\t\t\t\"filename\":    u.filename,\n\t\t\t\"md5\":         u.md5,\n\t\t\t\"ext\":         u.ext,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn token{}, err\n\t}\n\n\treturn token{\n\t\tresourceId: utils.Json.Get(body, \"result\", \"resourceId\").ToString(),\n\t\tobjectKey:  utils.Json.Get(body, \"result\", \"objectKey\").ToString(),\n\t\ttoken:      utils.Json.Get(body, \"result\", \"token\").ToString(),\n\t}, nil\n}\n\nfunc (u *uploader) publishInfo(resourceId string) error {\n\tbody, err := u.driver.request(\"https://music.163.com/api/upload/cloud/info/v2\", http.MethodPost, ReqOption{\n\t\tcrypto: \"weapi\",\n\t\tdata: map[string]string{\n\t\t\t\"md5\":        u.md5,\n\t\t\t\"filename\":   u.filename,\n\t\t\t\"song\":       u.meta.name,\n\t\t\t\"album\":      u.meta.album,\n\t\t\t\"artist\":     u.meta.artist,\n\t\t\t\"songid\":     u.meta.songId,\n\t\t\t\"resourceId\": resourceId,\n\t\t\t\"bitrate\":    \"999000\",\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = u.driver.request(\"https://interface.music.163.com/api/cloud/pub/v2\", http.MethodPost, ReqOption{\n\t\tcrypto: \"weapi\",\n\t\tdata: map[string]string{\n\t\t\t\"songid\": utils.Json.Get(body, \"songId\").ToString(),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (u *uploader) upload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tbucket := \"jd-musicrep-privatecloud-audio-public\"\n\ttoken, err := u.allocToken(bucket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbody, err := u.driver.request(\"https://wanproxy.127.net/lbs?version=1.0&bucketname=\"+bucket, http.MethodGet,\n\t\tReqOption{},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar resp HostsResp\n\terr = utils.Json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tobjectKey := strings.ReplaceAll(token.objectKey, \"/\", \"%2F\")\n\t_, err = u.driver.request(\n\t\tresp.Upload[0]+\"/\"+bucket+\"/\"+objectKey+\"?offset=0&complete=true&version=1.0\",\n\t\thttp.MethodPost,\n\t\tReqOption{\n\t\t\tstream: stream,\n\t\t\tup:     up,\n\t\t\tctx:    ctx,\n\t\t\theaders: map[string]string{\n\t\t\t\t\"x-nos-token\":    token.token,\n\t\t\t\t\"Content-Type\":   \"audio/mpeg\",\n\t\t\t\t\"Content-Length\": u.size,\n\t\t\t\t\"Content-MD5\":    u.md5,\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/netease_music/util.go",
    "content": "package netease_music\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc (d *NeteaseMusic) request(url, method string, opt ReqOption) ([]byte, error) {\n\treq := base.RestyClient.R()\n\n\treq.SetHeader(\"Cookie\", d.Addition.Cookie)\n\n\tif strings.Contains(url, \"music.163.com\") {\n\t\treq.SetHeader(\"Referer\", \"https://music.163.com\")\n\t}\n\n\tif opt.cookies != nil {\n\t\tfor _, cookie := range opt.cookies {\n\t\t\treq.SetCookie(cookie)\n\t\t}\n\t}\n\n\tif opt.headers != nil {\n\t\tfor header, value := range opt.headers {\n\t\t\treq.SetHeader(header, value)\n\t\t}\n\t}\n\n\tdata := opt.data\n\tif opt.crypto == \"weapi\" {\n\t\tdata = weapi(data)\n\t\tre, _ := regexp.Compile(`/\\w*api/`)\n\t\turl = re.ReplaceAllString(url, \"/weapi/\")\n\t} else if opt.crypto == \"eapi\" {\n\t\tch := new(Characteristic).fromDriver(d)\n\t\treq.SetCookies(ch.toCookies())\n\t\tdata = eapi(opt.url, ch.merge(data))\n\t\tre, _ := regexp.Compile(`/\\w*api/`)\n\t\turl = re.ReplaceAllString(url, \"/eapi/\")\n\t} else if opt.crypto == \"linuxapi\" {\n\t\tre, _ := regexp.Compile(`/\\w*api/`)\n\t\tdata = linuxapi(map[string]interface{}{\n\t\t\t\"url\":    re.ReplaceAllString(url, \"/api/\"),\n\t\t\t\"method\": method,\n\t\t\t\"params\": data,\n\t\t})\n\t\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\")\n\t\turl = \"https://music.163.com/api/linux/forward\"\n\t}\n\n\tif opt.ctx != nil {\n\t\treq.SetContext(opt.ctx)\n\t}\n\tif method == http.MethodPost {\n\t\tif opt.stream != nil {\n\t\t\tif opt.up == nil {\n\t\t\t\topt.up = func(_ float64) {}\n\t\t\t}\n\t\t\treq.SetContentLength(true)\n\t\t\treq.SetBody(driver.NewLimitedUploadStream(opt.ctx, &driver.ReaderUpdatingProgress{\n\t\t\t\tReader:         opt.stream,\n\t\t\t\tUpdateProgress: opt.up,\n\t\t\t}))\n\t\t} else {\n\t\t\treq.SetFormData(data)\n\t\t}\n\t\tres, err := req.Post(url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn res.Body(), nil\n\t}\n\n\tif method == http.MethodGet {\n\t\tres, err := req.Get(url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn res.Body(), nil\n\t}\n\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *NeteaseMusic) getSongObjs(args model.ListArgs) ([]model.Obj, error) {\n\tbody, err := d.request(\"https://music.163.com/weapi/v1/cloud/get\", http.MethodPost, ReqOption{\n\t\tcrypto: \"weapi\",\n\t\tdata: map[string]string{\n\t\t\t\"limit\":  strconv.FormatUint(d.Addition.SongLimit, 10),\n\t\t\t\"offset\": \"0\",\n\t\t},\n\t\tcookies: []*http.Cookie{\n\t\t\t{Name: \"os\", Value: \"pc\"},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp ListResp\n\terr = utils.Json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\td.fileMapByName = make(map[string]model.Obj)\n\tfiles := make([]model.Obj, 0, len(resp.Data))\n\tfor _, f := range resp.Data {\n\t\tsong := &model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tIsFolder: false,\n\t\t\t\tSize:     f.FileSize,\n\t\t\t\tName:     f.FileName,\n\t\t\t\tModified: time.UnixMilli(f.AddTime),\n\t\t\t\tID:       strconv.FormatInt(f.SongId, 10),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.SimpleSong.Al.PicUrl},\n\t\t}\n\t\td.fileMapByName[song.Name] = song\n\t\tfiles = append(files, song)\n\n\t\t// map song id for lyric\n\t\tlrcName := strings.Split(f.FileName, \".\")[0] + \".lrc\"\n\t\tlrc := &model.Object{\n\t\t\tIsFolder: false,\n\t\t\tName:     lrcName,\n\t\t\tPath:     path.Join(args.ReqPath, lrcName),\n\t\t\tID:       strconv.FormatInt(f.SongId, 10),\n\t\t}\n\t\td.fileMapByName[lrc.Name] = lrc\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *NeteaseMusic) getSongLink(file model.Obj) (*model.Link, error) {\n\tbody, err := d.request(\n\t\t\"https://music.163.com/api/song/enhance/player/url\", http.MethodPost, ReqOption{\n\t\t\tcrypto: \"linuxapi\",\n\t\t\tdata: map[string]string{\n\t\t\t\t\"ids\": \"[\" + file.GetID() + \"]\",\n\t\t\t\t\"br\":  \"999000\",\n\t\t\t},\n\t\t\tcookies: []*http.Cookie{\n\t\t\t\t{Name: \"os\", Value: \"pc\"},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp SongResp\n\terr = utils.Json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(resp.Data) < 1 {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\treturn &model.Link{URL: resp.Data[0].Url}, nil\n}\n\nfunc (d *NeteaseMusic) getLyricObj(file model.Obj) (model.Obj, error) {\n\tif lrc, ok := file.(*LyricObj); ok {\n\t\treturn lrc, nil\n\t}\n\n\tbody, err := d.request(\n\t\t\"https://music.163.com/api/song/lyric?_nmclfl=1\", http.MethodPost, ReqOption{\n\t\t\tdata: map[string]string{\n\t\t\t\t\"id\": file.GetID(),\n\t\t\t\t\"tv\": \"-1\",\n\t\t\t\t\"lv\": \"-1\",\n\t\t\t\t\"rv\": \"-1\",\n\t\t\t\t\"kv\": \"-1\",\n\t\t\t},\n\t\t\tcookies: []*http.Cookie{\n\t\t\t\t{Name: \"os\", Value: \"ios\"},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlyric := utils.Json.Get(body, \"lrc\", \"lyric\").ToString()\n\n\treturn &LyricObj{\n\t\tlyric: lyric,\n\t\tObject: model.Object{\n\t\t\tIsFolder: false,\n\t\t\tID:       file.GetID(),\n\t\t\tName:     file.GetName(),\n\t\t\tPath:     file.GetPath(),\n\t\t\tSize:     int64(len(lyric)),\n\t\t},\n\t}, nil\n}\n\nfunc (d *NeteaseMusic) removeSongObj(file model.Obj) error {\n\t_, err := d.request(\"http://music.163.com/weapi/cloud/del\", http.MethodPost, ReqOption{\n\t\tcrypto: \"weapi\",\n\t\tdata: map[string]string{\n\t\t\t\"songIds\": \"[\" + file.GetID() + \"]\",\n\t\t},\n\t})\n\n\treturn err\n}\n\nfunc (d *NeteaseMusic) putSongStream(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress) error {\n\ttmp, err := stream.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu := uploader{driver: d, file: tmp}\n\n\terr = u.init(stream)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = u.checkIfExisted()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttoken, err := u.allocToken()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif u.meta.needUpload {\n\t\terr = u.upload(ctx, stream, up)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = u.publishInfo(token.resourceId)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/onedrive/driver.go",
    "content": "package onedrive\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Onedrive struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n\troot        *Object\n\tmutex       sync.Mutex\n\tref         *Onedrive\n}\n\nfunc (d *Onedrive) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Onedrive) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Onedrive) Init(ctx context.Context) error {\n\tif d.ChunkSize < 1 {\n\t\td.ChunkSize = 5\n\t}\n\tif d.ref != nil {\n\t\treturn nil\n\t}\n\treturn d.refreshToken()\n}\n\nfunc (d *Onedrive) InitReference(refStorage driver.Driver) error {\n\tif ref, ok := refStorage.(*Onedrive); ok {\n\t\td.ref = ref\n\t\treturn nil\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *Onedrive) Drop(ctx context.Context) error {\n\td.ref = nil\n\treturn nil\n}\n\nfunc (d *Onedrive) GetRoot(ctx context.Context) (model.Obj, error) {\n\tif d.root != nil {\n\t\treturn d.root, nil\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\troot := &Object{\n\t\tObjThumb: model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       \"root\",\n\t\t\t\tPath:     d.RootFolderPath,\n\t\t\t\tName:     \"root\",\n\t\t\t\tSize:     0,\n\t\t\t\tModified: d.Modified,\n\t\t\t\tCtime:    d.Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t},\n\t\t},\n\t\tParentID: \"\",\n\t}\n\tif !utils.PathEqual(d.RootFolderPath, \"/\") {\n\t\t// get root folder id\n\t\turl := d.GetMetaUrl(false, d.RootFolderPath)\n\t\tvar resp struct {\n\t\t\tId string `json:\"id\"`\n\t\t}\n\t\t_, err := d.Request(url, http.MethodGet, nil, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\troot.ID = resp.Id\n\t}\n\td.root = root\n\treturn d.root, nil\n}\n\nfunc (d *Onedrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\tobj := fileToObj(src, dir.GetID())\n\t\tobj.Path = path.Join(dir.GetPath(), obj.GetName())\n\t\treturn obj, nil\n\t})\n}\n\nfunc (d *Onedrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf, err := d.GetFile(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif f.File == nil {\n\t\treturn nil, errs.NotFile\n\t}\n\tu := f.Url\n\tif d.CustomHost != \"\" {\n\t\t_u, err := url.Parse(f.Url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t_u.Host = d.CustomHost\n\t\tu = _u.String()\n\t}\n\treturn &model.Link{\n\t\tURL: u,\n\t}, nil\n}\n\nfunc (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\turl := d.GetMetaUrl(false, parentDir.GetPath()) + \"/children\"\n\tdata := base.Json{\n\t\t\"name\":                              dirName,\n\t\t\"folder\":                            base.Json{},\n\t\t\"@microsoft.graph.conflictBehavior\": \"rename\",\n\t}\n\t// todo 修复文件夹 ctime/mtime, onedrive 可在 data 里设置 fileSystemInfo 字段, 但是此接口未提供 ctime/mtime\n\t_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Onedrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tparentPath := \"\"\n\tif dstDir.GetID() == \"\" {\n\t\tparentPath = dstDir.GetPath()\n\t\tif utils.PathEqual(parentPath, \"/\") {\n\t\t\tparentPath = path.Join(\"/drive/root\", parentPath)\n\t\t} else {\n\t\t\tparentPath = path.Join(\"/drive/root:/\", parentPath)\n\t\t}\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"id\":   dstDir.GetID(),\n\t\t\t\"path\": parentPath,\n\t\t},\n\t\t\"name\": srcObj.GetName(),\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath())\n\t_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Onedrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tvar parentID string\n\tif o, ok := srcObj.(*Object); ok {\n\t\tparentID = o.ParentID\n\t} else {\n\t\treturn fmt.Errorf(\"srcObj is not Object\")\n\t}\n\tif parentID == \"\" {\n\t\tparentID = \"root\"\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"id\": parentID,\n\t\t},\n\t\t\"name\": newName,\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath())\n\t_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Onedrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdst, err := d.GetFile(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"driveId\": dst.ParentReference.DriveId,\n\t\t\t\"id\":      dst.Id,\n\t\t},\n\t\t\"name\": srcObj.GetName(),\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath()) + \"/copy\"\n\t_, err = d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Onedrive) Remove(ctx context.Context, obj model.Obj) error {\n\turl := d.GetMetaUrl(false, obj.GetPath())\n\t_, err := d.Request(url, http.MethodDelete, nil, nil)\n\treturn err\n}\n\nfunc (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tvar err error\n\tif stream.GetSize() <= 4*1024*1024 {\n\t\terr = d.upSmall(ctx, dstDir, stream)\n\t} else {\n\t\terr = d.upBig(ctx, dstDir, stream, up)\n\t}\n\treturn err\n}\n\nfunc (d *Onedrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif d.DisableDiskUsage {\n\t\treturn nil, errs.NotImplement\n\t}\n\tdrive, err := d.getDrive(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: drive.Quota.Total,\n\t\t\tUsedSpace:  drive.Quota.Used,\n\t\t},\n\t}, nil\n}\n\nfunc (d *Onedrive) GetDirectUploadTools() []string {\n\tif !d.EnableDirectUpload {\n\t\treturn nil\n\t}\n\treturn []string{\"HttpDirect\"}\n}\n\n// GetDirectUploadInfo returns the direct upload info for OneDrive\nfunc (d *Onedrive) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {\n\tif !d.EnableDirectUpload {\n\t\treturn nil, errs.NotImplement\n\t}\n\treturn d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))\n}\n\nvar _ driver.Driver = (*Onedrive)(nil)\n"
  },
  {
    "path": "drivers/onedrive/meta.go",
    "content": "package onedrive\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tRegion             string `json:\"region\" type:\"select\" required:\"true\" options:\"global,cn,us,de\" default:\"global\"`\n\tIsSharepoint       bool   `json:\"is_sharepoint\"`\n\tUseOnlineAPI       bool   `json:\"use_online_api\" default:\"true\"`\n\tAPIAddress         string `json:\"api_url_address\" default:\"https://api.oplist.org/onedrive/renewapi\"`\n\tClientID           string `json:\"client_id\"`\n\tClientSecret       string `json:\"client_secret\"`\n\tRedirectUri        string `json:\"redirect_uri\" required:\"true\" default:\"https://api.oplist.org/onedrive/callback\"`\n\tRefreshToken       string `json:\"refresh_token\" required:\"true\"`\n\tSiteId             string `json:\"site_id\"`\n\tChunkSize          int64  `json:\"chunk_size\" type:\"number\" default:\"5\"`\n\tCustomHost         string `json:\"custom_host\" help:\"Custom host for onedrive download link\"`\n\tDisableDiskUsage   bool   `json:\"disable_disk_usage\" default:\"false\"`\n\tEnableDirectUpload bool   `json:\"enable_direct_upload\" default:\"false\" help:\"Enable direct upload from client to OneDrive\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Onedrive\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Onedrive{}\n\t})\n}\n"
  },
  {
    "path": "drivers/onedrive/types.go",
    "content": "package onedrive\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Host struct {\n\tOauth string\n\tApi   string\n}\n\ntype TokenErr struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\ntype RespErr struct {\n\tError struct {\n\t\tCode    string `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\ntype File struct {\n\tId             string               `json:\"id\"`\n\tName           string               `json:\"name\"`\n\tSize           int64                `json:\"size\"`\n\tFileSystemInfo *FileSystemInfoFacet `json:\"fileSystemInfo\"`\n\tUrl            string               `json:\"@microsoft.graph.downloadUrl\"`\n\tFile           *struct {\n\t\tMimeType string `json:\"mimeType\"`\n\t} `json:\"file\"`\n\tThumbnails []struct {\n\t\tMedium struct {\n\t\t\tUrl string `json:\"url\"`\n\t\t} `json:\"medium\"`\n\t} `json:\"thumbnails\"`\n\tParentReference struct {\n\t\tDriveId string `json:\"driveId\"`\n\t} `json:\"parentReference\"`\n}\n\ntype Object struct {\n\tmodel.ObjThumb\n\tParentID string\n}\n\nfunc fileToObj(f File, parentID string) *Object {\n\tthumb := \"\"\n\tif len(f.Thumbnails) > 0 {\n\t\tthumb = f.Thumbnails[0].Medium.Url\n\t}\n\treturn &Object{\n\t\tObjThumb: model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       f.Id,\n\t\t\t\tName:     f.Name,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tModified: f.FileSystemInfo.LastModifiedDateTime,\n\t\t\t\tIsFolder: f.File == nil,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: thumb},\n\t\t\t//Url:       model.Url{Url: f.Url},\n\t\t},\n\t\tParentID: parentID,\n\t}\n}\n\ntype Files struct {\n\tValue    []File `json:\"value\"`\n\tNextLink string `json:\"@odata.nextLink\"`\n}\n\n// Metadata represents a request to update Metadata.\n// It includes only the writeable properties.\n// omitempty is intentionally included for all, per https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online#request-body\ntype Metadata struct {\n\tDescription    string               `json:\"description,omitempty\"`    // Provides a user-visible description of the item. Read-write. Only on OneDrive Personal. Undocumented limit of 1024 characters.\n\tFileSystemInfo *FileSystemInfoFacet `json:\"fileSystemInfo,omitempty\"` // File system information on client. Read-write.\n}\n\n// FileSystemInfoFacet contains properties that are reported by the\n// device's local file system for the local version of an item. This\n// facet can be used to specify the last modified date or created date\n// of the item as it was on the local device.\ntype FileSystemInfoFacet struct {\n\tCreatedDateTime      time.Time `json:\"createdDateTime,omitempty\"`      // The UTC date and time the file was created on a client.\n\tLastModifiedDateTime time.Time `json:\"lastModifiedDateTime,omitempty\"` // The UTC date and time the file was last modified on a client.\n}\n\ntype DriveResp struct {\n\tID        string `json:\"id\"`\n\tDriveType string `json:\"driveType\"`\n\tQuota     struct {\n\t\tDeleted   uint64 `json:\"deleted\"`\n\t\tRemaining int64  `json:\"remaining\"`\n\t\tState     string `json:\"state\"`\n\t\tTotal     int64  `json:\"total\"`\n\t\tUsed      int64  `json:\"used\"`\n\t} `json:\"quota\"`\n}\n"
  },
  {
    "path": "drivers/onedrive/util.go",
    "content": "package onedrive\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar onedriveHostMap = map[string]Host{\n\t\"global\": {\n\t\tOauth: \"https://login.microsoftonline.com\",\n\t\tApi:   \"https://graph.microsoft.com\",\n\t},\n\t\"cn\": {\n\t\tOauth: \"https://login.chinacloudapi.cn\",\n\t\tApi:   \"https://microsoftgraph.chinacloudapi.cn\",\n\t},\n\t\"us\": {\n\t\tOauth: \"https://login.microsoftonline.us\",\n\t\tApi:   \"https://graph.microsoft.us\",\n\t},\n\t\"de\": {\n\t\tOauth: \"https://login.microsoftonline.de\",\n\t\tApi:   \"https://graph.microsoft.de\",\n\t},\n}\n\nfunc (d *Onedrive) GetMetaUrl(auth bool, path string) string {\n\thost, _ := onedriveHostMap[d.Region]\n\tpath = utils.EncodePath(path, true)\n\tif auth {\n\t\treturn host.Oauth\n\t}\n\tif d.IsSharepoint {\n\t\tif path == \"/\" || path == \"\\\\\" {\n\t\t\treturn fmt.Sprintf(\"%s/v1.0/sites/%s/drive/root\", host.Api, d.SiteId)\n\t\t} else {\n\t\t\treturn fmt.Sprintf(\"%s/v1.0/sites/%s/drive/root:%s:\", host.Api, d.SiteId, path)\n\t\t}\n\t} else {\n\t\tif path == \"/\" || path == \"\\\\\" {\n\t\t\treturn fmt.Sprintf(\"%s/v1.0/me/drive/root\", host.Api)\n\t\t} else {\n\t\t\treturn fmt.Sprintf(\"%s/v1.0/me/drive/root:%s:\", host.Api, path)\n\t\t}\n\t}\n}\n\nfunc (d *Onedrive) refreshToken() error {\n\tvar err error\n\tfor i := 0; i < 3; i++ {\n\t\terr = d._refreshToken()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *Onedrive) _refreshToken() error {\n\t// 使用在线API刷新Token，无需ClientID和ClientSecret\n\tif d.UseOnlineAPI && len(d.APIAddress) > 0 {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"onedrive_pr\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\td.RefreshToken = resp.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\t// 使用本地客户端的情况下检查是否为空\n\tif d.ClientID == \"\" || d.ClientSecret == \"\" {\n\t\treturn fmt.Errorf(\"empty ClientID or ClientSecret\")\n\t}\n\t// 走原有的刷新逻辑\n\turl := d.GetMetaUrl(true, \"\") + \"/common/oauth2/v2.0/token\"\n\tvar resp base.TokenResp\n\tvar e TokenErr\n\t_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{\n\t\t\"grant_type\":    \"refresh_token\",\n\t\t\"client_id\":     d.ClientID,\n\t\t\"client_secret\": d.ClientSecret,\n\t\t\"redirect_uri\":  d.RedirectUri,\n\t\t\"refresh_token\": d.RefreshToken,\n\t}).Post(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(\"%s\", e.ErrorDescription)\n\t}\n\tif resp.RefreshToken == \"\" {\n\t\treturn errs.EmptyToken\n\t}\n\td.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {\n\tif d.ref != nil {\n\t\treturn d.ref.Request(url, method, callback, resp)\n\t}\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e RespErr\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Error.Code != \"\" {\n\t\tif e.Error.Code == \"InvalidAuthenticationToken\" && !utils.IsBool(noRetry...) {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.Request(url, method, callback, resp)\n\t\t}\n\t\treturn nil, errors.New(e.Error.Message)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Onedrive) getFiles(path string) ([]File, error) {\n\tvar res []File\n\tnextLink := d.GetMetaUrl(false, path) + \"/children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference\"\n\tfor nextLink != \"\" {\n\t\tvar files Files\n\t\t_, err := d.Request(nextLink, http.MethodGet, nil, &files)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, files.Value...)\n\t\tnextLink = files.NextLink\n\t}\n\treturn res, nil\n}\n\nfunc (d *Onedrive) GetFile(path string) (*File, error) {\n\tvar file File\n\tu := d.GetMetaUrl(false, path)\n\t_, err := d.Request(u, http.MethodGet, nil, &file)\n\treturn &file, err\n}\n\nfunc (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {\n\tfilepath := stdpath.Join(dstDir.GetPath(), stream.GetName())\n\t// 1. upload new file\n\t// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online\n\turl := d.GetMetaUrl(false, filepath) + \"/content\"\n\t_, err := d.Request(url, http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, stream)).SetContext(ctx)\n\t}, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"onedrive: Failed to upload new file(path=%v): %w\", filepath, err)\n\t}\n\n\t// 2. update metadata\n\terr = d.updateMetadata(ctx, stream, filepath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"onedrive: Failed to update file(path=%v) metadata: %w\", filepath, err)\n\t}\n\treturn nil\n}\n\nfunc (d *Onedrive) updateMetadata(ctx context.Context, stream model.FileStreamer, filepath string) error {\n\turl := d.GetMetaUrl(false, filepath)\n\tmetadata := toAPIMetadata(stream)\n\t// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online\n\t_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(metadata).SetContext(ctx)\n\t}, nil)\n\treturn err\n}\n\nfunc toAPIMetadata(stream model.FileStreamer) Metadata {\n\tmetadata := Metadata{\n\t\tFileSystemInfo: &FileSystemInfoFacet{},\n\t}\n\tif !stream.ModTime().IsZero() {\n\t\tmetadata.FileSystemInfo.LastModifiedDateTime = stream.ModTime()\n\t}\n\tif !stream.CreateTime().IsZero() {\n\t\tmetadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()\n\t}\n\tif stream.CreateTime().IsZero() && !stream.ModTime().IsZero() {\n\t\tmetadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()\n\t}\n\treturn metadata\n}\n\nfunc (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\turl := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + \"/createUploadSession\"\n\tmetadata := map[string]any{\"item\": toAPIMetadata(stream)}\n\tres, err := d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(metadata).SetContext(ctx)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tDEFAULT := d.ChunkSize * 1024 * 1024\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tvar finish int64 = 0\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[Onedrive] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\t\t\treturn errors.New(string(data))\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t}\n\treturn nil\n}\n\nfunc (d *Onedrive) getDrive(ctx context.Context) (*DriveResp, error) {\n\tvar api string\n\thost, _ := onedriveHostMap[d.Region]\n\tif d.IsSharepoint {\n\t\tapi = fmt.Sprintf(\"%s/v1.0/sites/%s/drive\", host.Api, d.SiteId)\n\t} else {\n\t\tapi = fmt.Sprintf(\"%s/v1.0/me/drive\", host.Api)\n\t}\n\tvar resp DriveResp\n\t_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Onedrive) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {\n\t// Create upload session\n\turl := d.GetMetaUrl(false, path) + \"/createUploadSession\"\n\tmetadata := map[string]any{\n\t\t\"item\": map[string]any{\n\t\t\t\"@microsoft.graph.conflictBehavior\": \"rename\",\n\t\t},\n\t}\n\n\tres, err := d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(metadata).SetContext(ctx)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tif uploadUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"failed to get upload URL from response\")\n\t}\n\treturn &model.HttpDirectUploadInfo{\n\t\tUploadURL: uploadUrl,\n\t\tChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes\n\t\tMethod:    \"PUT\",\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/onedrive_app/driver.go",
    "content": "package onedrive_app\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype OnedriveAPP struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n\troot        *Object\n\tmutex       sync.Mutex\n}\n\nfunc (d *OnedriveAPP) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *OnedriveAPP) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *OnedriveAPP) Init(ctx context.Context) error {\n\tif d.ChunkSize < 1 {\n\t\td.ChunkSize = 5\n\t}\n\treturn d.accessToken()\n}\n\nfunc (d *OnedriveAPP) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *OnedriveAPP) GetRoot(ctx context.Context) (model.Obj, error) {\n\tif d.root != nil {\n\t\treturn d.root, nil\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\troot := &Object{\n\t\tObjThumb: model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       \"root\",\n\t\t\t\tPath:     d.RootFolderPath,\n\t\t\t\tName:     \"root\",\n\t\t\t\tSize:     0,\n\t\t\t\tModified: d.Modified,\n\t\t\t\tCtime:    d.Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t},\n\t\t},\n\t\tParentID: \"\",\n\t}\n\tif !utils.PathEqual(d.RootFolderPath, \"/\") {\n\t\t// get root folder id\n\t\turl := d.GetMetaUrl(false, d.RootFolderPath)\n\t\tvar resp struct {\n\t\t\tId string `json:\"id\"`\n\t\t}\n\t\t_, err := d.Request(url, http.MethodGet, nil, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\troot.ID = resp.Id\n\t}\n\td.root = root\n\treturn d.root, nil\n}\n\nfunc (d *OnedriveAPP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\tobj := fileToObj(src, dir.GetID())\n\t\tobj.Path = path.Join(dir.GetPath(), obj.GetName())\n\t\treturn obj, nil\n\t})\n}\n\nfunc (d *OnedriveAPP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf, err := d.GetFile(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif f.File == nil {\n\t\treturn nil, errs.NotFile\n\t}\n\tu := f.Url\n\tif d.CustomHost != \"\" {\n\t\t_u, err := url.Parse(f.Url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t_u.Host = d.CustomHost\n\t\tu = _u.String()\n\t}\n\treturn &model.Link{\n\t\tURL: u,\n\t}, nil\n}\n\nfunc (d *OnedriveAPP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\turl := d.GetMetaUrl(false, parentDir.GetPath()) + \"/children\"\n\tdata := base.Json{\n\t\t\"name\":                              dirName,\n\t\t\"folder\":                            base.Json{},\n\t\t\"@microsoft.graph.conflictBehavior\": \"rename\",\n\t}\n\t_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tparentPath := \"\"\n\tif dstDir.GetID() == \"\" {\n\t\tparentPath = dstDir.GetPath()\n\t\tif utils.PathEqual(parentPath, \"/\") {\n\t\t\tparentPath = path.Join(\"/drive/root\", parentPath)\n\t\t} else {\n\t\t\tparentPath = path.Join(\"/drive/root:/\", parentPath)\n\t\t}\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"id\":   dstDir.GetID(),\n\t\t\t\"path\": parentPath,\n\t\t},\n\t\t\"name\": srcObj.GetName(),\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath())\n\t_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tvar parentID string\n\tif o, ok := srcObj.(*Object); ok {\n\t\tparentID = o.ParentID\n\t} else {\n\t\treturn fmt.Errorf(\"srcObj is not Object\")\n\t}\n\tif parentID == \"\" {\n\t\tparentID = \"root\"\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"id\": parentID,\n\t\t},\n\t\t\"name\": newName,\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath())\n\t_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdst, err := d.GetFile(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata := base.Json{\n\t\t\"parentReference\": base.Json{\n\t\t\t\"driveId\": dst.ParentReference.DriveId,\n\t\t\t\"id\":      dst.Id,\n\t\t},\n\t\t\"name\": srcObj.GetName(),\n\t}\n\turl := d.GetMetaUrl(false, srcObj.GetPath()) + \"/copy\"\n\t_, err = d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) Remove(ctx context.Context, obj model.Obj) error {\n\turl := d.GetMetaUrl(false, obj.GetPath())\n\t_, err := d.Request(url, http.MethodDelete, nil, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tvar err error\n\tif stream.GetSize() <= 4*1024*1024 {\n\t\terr = d.upSmall(ctx, dstDir, stream)\n\t} else {\n\t\terr = d.upBig(ctx, dstDir, stream, up)\n\t}\n\treturn err\n}\n\nfunc (d *OnedriveAPP) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif d.DisableDiskUsage {\n\t\treturn nil, errs.NotImplement\n\t}\n\tdrive, err := d.getDrive(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: drive.Quota.Total,\n\t\t\tUsedSpace:  drive.Quota.Used,\n\t\t},\n\t}, nil\n}\n\nfunc (d *OnedriveAPP) GetDirectUploadTools() []string {\n\tif !d.EnableDirectUpload {\n\t\treturn nil\n\t}\n\treturn []string{\"HttpDirect\"}\n}\n\nfunc (d *OnedriveAPP) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {\n\tif !d.EnableDirectUpload {\n\t\treturn nil, errs.NotImplement\n\t}\n\treturn d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))\n}\n\nvar _ driver.Driver = (*OnedriveAPP)(nil)\n"
  },
  {
    "path": "drivers/onedrive_app/meta.go",
    "content": "package onedrive_app\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tRegion             string `json:\"region\" type:\"select\" required:\"true\" options:\"global,cn,us,de\" default:\"global\"`\n\tClientID           string `json:\"client_id\" required:\"true\"`\n\tClientSecret       string `json:\"client_secret\" required:\"true\"`\n\tTenantID           string `json:\"tenant_id\"`\n\tEmail              string `json:\"email\"`\n\tChunkSize          int64  `json:\"chunk_size\" type:\"number\" default:\"5\"`\n\tCustomHost         string `json:\"custom_host\" help:\"Custom host for onedrive download link\"`\n\tDisableDiskUsage   bool   `json:\"disable_disk_usage\" default:\"false\"`\n\tEnableDirectUpload bool   `json:\"enable_direct_upload\" default:\"false\" help:\"Enable direct upload from client to OneDrive\"`\n}\n\nvar config = driver.Config{\n\tName:        \"OnedriveAPP\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &OnedriveAPP{}\n\t})\n}\n"
  },
  {
    "path": "drivers/onedrive_app/types.go",
    "content": "package onedrive_app\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Host struct {\n\tOauth string\n\tApi   string\n}\n\ntype TokenErr struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\ntype RespErr struct {\n\tError struct {\n\t\tCode    string `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\ntype File struct {\n\tId                   string    `json:\"id\"`\n\tName                 string    `json:\"name\"`\n\tSize                 int64     `json:\"size\"`\n\tLastModifiedDateTime time.Time `json:\"lastModifiedDateTime\"`\n\tUrl                  string    `json:\"@microsoft.graph.downloadUrl\"`\n\tFile                 *struct {\n\t\tMimeType string `json:\"mimeType\"`\n\t} `json:\"file\"`\n\tThumbnails []struct {\n\t\tMedium struct {\n\t\t\tUrl string `json:\"url\"`\n\t\t} `json:\"medium\"`\n\t} `json:\"thumbnails\"`\n\tParentReference struct {\n\t\tDriveId string `json:\"driveId\"`\n\t} `json:\"parentReference\"`\n}\n\ntype Object struct {\n\tmodel.ObjThumb\n\tParentID string\n}\n\nfunc fileToObj(f File, parentID string) *Object {\n\tthumb := \"\"\n\tif len(f.Thumbnails) > 0 {\n\t\tthumb = f.Thumbnails[0].Medium.Url\n\t}\n\treturn &Object{\n\t\tObjThumb: model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       f.Id,\n\t\t\t\tName:     f.Name,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tModified: f.LastModifiedDateTime,\n\t\t\t\tIsFolder: f.File == nil,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: thumb},\n\t\t\t//Url:       model.Url{Url: f.Url},\n\t\t},\n\t\tParentID: parentID,\n\t}\n}\n\ntype Files struct {\n\tValue    []File `json:\"value\"`\n\tNextLink string `json:\"@odata.nextLink\"`\n}\n\ntype DriveResp struct {\n\tID        string `json:\"id\"`\n\tDriveType string `json:\"driveType\"`\n\tQuota     struct {\n\t\tDeleted   uint64 `json:\"deleted\"`\n\t\tRemaining int64  `json:\"remaining\"`\n\t\tState     string `json:\"state\"`\n\t\tTotal     int64  `json:\"total\"`\n\t\tUsed      int64  `json:\"used\"`\n\t} `json:\"quota\"`\n}\n"
  },
  {
    "path": "drivers/onedrive_app/util.go",
    "content": "package onedrive_app\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar onedriveHostMap = map[string]Host{\n\t\"global\": {\n\t\tOauth: \"https://login.microsoftonline.com\",\n\t\tApi:   \"https://graph.microsoft.com\",\n\t},\n\t\"cn\": {\n\t\tOauth: \"https://login.chinacloudapi.cn\",\n\t\tApi:   \"https://microsoftgraph.chinacloudapi.cn\",\n\t},\n\t\"us\": {\n\t\tOauth: \"https://login.microsoftonline.us\",\n\t\tApi:   \"https://graph.microsoft.us\",\n\t},\n\t\"de\": {\n\t\tOauth: \"https://login.microsoftonline.de\",\n\t\tApi:   \"https://graph.microsoft.de\",\n\t},\n}\n\nfunc (d *OnedriveAPP) GetMetaUrl(auth bool, path string) string {\n\thost := onedriveHostMap[d.Region]\n\tpath = utils.EncodePath(path, true)\n\tif auth {\n\t\treturn host.Oauth\n\t}\n\tif path == \"/\" || path == \"\\\\\" {\n\t\treturn fmt.Sprintf(\"%s/v1.0/users/%s/drive/root\", host.Api, d.Email)\n\t}\n\treturn fmt.Sprintf(\"%s/v1.0/users/%s/drive/root:%s:\", host.Api, d.Email, path)\n}\n\nfunc (d *OnedriveAPP) accessToken() error {\n\tvar err error\n\tfor i := 0; i < 3; i++ {\n\t\terr = d._accessToken()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *OnedriveAPP) _accessToken() error {\n\turl := d.GetMetaUrl(true, \"\") + \"/\" + d.TenantID + \"/oauth2/token\"\n\tvar resp base.TokenResp\n\tvar e TokenErr\n\t_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{\n\t\t\"grant_type\":    \"client_credentials\",\n\t\t\"client_id\":     d.ClientID,\n\t\t\"client_secret\": d.ClientSecret,\n\t\t\"resource\":      onedriveHostMap[d.Region].Api + \"/\",\n\t\t\"scope\":         onedriveHostMap[d.Region].Api + \"/.default\",\n\t}).Post(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(\"%s\", e.ErrorDescription)\n\t}\n\tif resp.AccessToken == \"\" {\n\t\treturn errs.EmptyToken\n\t}\n\td.AccessToken = resp.AccessToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e RespErr\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Error.Code != \"\" {\n\t\tif e.Error.Code == \"InvalidAuthenticationToken\" && !utils.IsBool(noRetry...) {\n\t\t\terr = d.accessToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.Request(url, method, callback, resp)\n\t\t}\n\t\treturn nil, errors.New(e.Error.Message)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *OnedriveAPP) getFiles(path string) ([]File, error) {\n\tvar res []File\n\tnextLink := d.GetMetaUrl(false, path) + \"/children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference\"\n\tfor nextLink != \"\" {\n\t\tvar files Files\n\t\t_, err := d.Request(nextLink, http.MethodGet, nil, &files)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, files.Value...)\n\t\tnextLink = files.NextLink\n\t}\n\treturn res, nil\n}\n\nfunc (d *OnedriveAPP) GetFile(path string) (*File, error) {\n\tvar file File\n\tu := d.GetMetaUrl(false, path)\n\t_, err := d.Request(u, http.MethodGet, nil, &file)\n\treturn &file, err\n}\n\nfunc (d *OnedriveAPP) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {\n\turl := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + \"/content\"\n\t_, err := d.Request(url, http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, stream)).SetContext(ctx)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\turl := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + \"/createUploadSession\"\n\tres, err := d.Request(url, http.MethodPost, nil, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tDEFAULT := d.ChunkSize * 1024 * 1024\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(DEFAULT), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tvar finish int64 = 0\n\tfor finish < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tleft := stream.GetSize() - finish\n\t\tbyteSize := min(left, DEFAULT)\n\t\tutils.Log.Debugf(\"[OnedriveAPP] upload range: %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize())\n\t\trd, err := ss.GetSectionReader(finish, byteSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\trd.Seek(0, io.SeekStart)\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.ContentLength = byteSize\n\t\t\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\t\t\tres, err := base.HttpClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\t\t\tswitch {\n\t\t\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\t\t\treturn fmt.Errorf(\"server error: %d\", res.StatusCode)\n\t\t\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\t\t\treturn errors.New(string(data))\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second),\n\t\t)\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinish += byteSize\n\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t}\n\treturn nil\n}\n\nfunc (d *OnedriveAPP) getDrive(ctx context.Context) (*DriveResp, error) {\n\thost, _ := onedriveHostMap[d.Region]\n\tapi := fmt.Sprintf(\"%s/v1.0/users/%s/drive\", host.Api, d.Email)\n\tvar resp DriveResp\n\t_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &resp, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *OnedriveAPP) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {\n\t// Create upload session\n\turl := d.GetMetaUrl(false, path) + \"/createUploadSession\"\n\tmetadata := map[string]any{\n\t\t\"item\": map[string]any{\n\t\t\t\"@microsoft.graph.conflictBehavior\": \"rename\",\n\t\t},\n\t}\n\n\tres, err := d.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(metadata).SetContext(ctx)\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tif uploadUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"failed to get upload URL from response\")\n\t}\n\treturn &model.HttpDirectUploadInfo{\n\t\tUploadURL: uploadUrl,\n\t\tChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes\n\t\tMethod:    \"PUT\",\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/onedrive_sharelink/driver.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst headerTTL = 25 * time.Minute\n\ntype OnedriveSharelink struct {\n\tmodel.Storage\n\tcron *cron.Cron\n\tAddition\n\n\theaderMu sync.RWMutex\n\tsg       singleflight.Group[http.Header]\n}\n\nfunc (d *OnedriveSharelink) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *OnedriveSharelink) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *OnedriveSharelink) Init(ctx context.Context) error {\n\t// Initialize error variable\n\tvar err error\n\n\t// If there is \"-my\" in the URL, it is NOT a SharePoint link\n\td.IsSharepoint = !strings.Contains(d.ShareLinkURL, \"-my\")\n\n\t// Initialize cron job to run every hour\n\td.cron = cron.NewCron(time.Hour * 1)\n\td.cron.Do(func() {\n\t\tvar err error\n\t\th, err := d.getHeaders(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t\treturn\n\t\t}\n\t\td.storeHeaders(h)\n\t})\n\n\t// Get initial headers\n\th, err := d.getHeaders(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.storeHeaders(h)\n\n\treturn nil\n}\n\nfunc (d *OnedriveSharelink) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(ctx, dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Convert the slice of files to the required model.Obj format\n\treturn utils.SliceConvert(files, func(src Item) (model.Obj, error) {\n\t\tobj := fileToObj(src)\n\t\tobj.Path = stdpath.Join(dir.GetPath(), obj.GetName())\n\t\treturn obj, nil\n\t})\n}\n\nfunc (d *OnedriveSharelink) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\t// Get the unique ID of the file\n\tuniqueId := file.GetID()\n\t// Cut the first char and the last char\n\tuniqueId = uniqueId[1 : len(uniqueId)-1]\n\turl := d.downloadLinkPrefix + uniqueId\n\n\theader, err := d.getValidHeaders(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL:    url,\n\t\tHeader: header,\n\t\tRangeReader: rangeReaderFunc(func(ctx context.Context, hr http_range.Range) (io.ReadCloser, error) {\n\t\t\treturn d.rangeReadWithRefresh(ctx, url, hr)\n\t\t}),\n\t}, nil\n}\n\nfunc (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t// TODO create folder, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *OnedriveSharelink) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO move obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *OnedriveSharelink) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t// TODO rename obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *OnedriveSharelink) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO copy obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\t// TODO upload file, optional\n\treturn errs.NotImplement\n}\n\n//func (d *OnedriveSharelink) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*OnedriveSharelink)(nil)\n\n// rangeReadWithRefresh tries once with current headers, and if the response\n// looks invalid (error status or html login page), it refreshes headers and retries.\nfunc (d *OnedriveSharelink) rangeReadWithRefresh(ctx context.Context, url string, hr http_range.Range) (io.ReadCloser, error) {\n\ttryOnce := func(header http.Header) (io.ReadCloser, error) {\n\t\th := cloneHeader(header)\n\t\tif h == nil {\n\t\t\th = http.Header{}\n\t\t}\n\t\th = http_range.ApplyRangeToHttpHeader(hr, h)\n\t\tresp, err := net.RequestHttp(ctx, http.MethodGet, h, url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tct := strings.ToLower(resp.Header.Get(\"Content-Type\"))\n\t\tif strings.Contains(ct, \"text/html\") {\n\t\t\t_ = resp.Body.Close()\n\t\t\treturn nil, fmt.Errorf(\"unexpected html response\")\n\t\t}\n\t\treturn resp.Body, nil\n\t}\n\n\theader, err := d.getValidHeaders(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif body, err := tryOnce(header); err == nil {\n\t\treturn body, nil\n\t}\n\n\t// refresh and retry once\n\theader, err = d.refreshHeaders(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tryOnce(header)\n}\n\ntype rangeReaderFunc func(ctx context.Context, hr http_range.Range) (io.ReadCloser, error)\n\nfunc (f rangeReaderFunc) RangeRead(ctx context.Context, hr http_range.Range) (io.ReadCloser, error) {\n\treturn f(ctx, hr)\n}\n\nfunc cloneHeader(header http.Header) http.Header {\n\tif header == nil {\n\t\treturn nil\n\t}\n\treturn header.Clone()\n}\n\nfunc (d *OnedriveSharelink) headerSnapshot() http.Header {\n\td.headerMu.RLock()\n\tdefer d.headerMu.RUnlock()\n\treturn cloneHeader(d.Headers)\n}\n\nfunc (d *OnedriveSharelink) storeHeaders(header http.Header) {\n\tif header == nil {\n\t\treturn\n\t}\n\td.headerMu.Lock()\n\td.Headers = header\n\td.HeaderTime = time.Now().Unix()\n\td.headerMu.Unlock()\n}\n\nfunc (d *OnedriveSharelink) headersExpired() bool {\n\td.headerMu.RLock()\n\tdefer d.headerMu.RUnlock()\n\treturn time.Since(time.Unix(d.HeaderTime, 0)) > headerTTL\n}\n\nfunc (d *OnedriveSharelink) refreshHeaders(ctx context.Context) (http.Header, error) {\n\theader, err, _ := d.sg.Do(\"refresh\", func() (http.Header, error) {\n\t\th, e := d.getHeaders(ctx)\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\td.storeHeaders(h)\n\t\treturn h, nil\n\t})\n\treturn header, err\n}\n\nfunc (d *OnedriveSharelink) getValidHeaders(ctx context.Context) (http.Header, error) {\n\tif h := d.headerSnapshot(); h != nil && !d.headersExpired() {\n\t\treturn h, nil\n\t}\n\th, err := d.refreshHeaders(ctx)\n\tif err != nil {\n\t\tif h2 := d.headerSnapshot(); h2 != nil {\n\t\t\tlog.Warnf(\"onedrive_sharelink: use cached headers after refresh failure: %+v\", err)\n\t\t\treturn h2, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn h, nil\n}\n"
  },
  {
    "path": "drivers/onedrive_sharelink/meta.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tShareLinkURL       string `json:\"url\" required:\"true\"`\n\tShareLinkPassword  string `json:\"password\"`\n\tIsSharepoint       bool\n\tdownloadLinkPrefix string\n\tHeaders            http.Header\n\tHeaderTime         int64\n}\n\nvar config = driver.Config{\n\tName:        \"Onedrive Sharelink\",\n\tOnlyProxy:   true,\n\tNoUpload:    true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &OnedriveSharelink{}\n\t})\n}\n"
  },
  {
    "path": "drivers/onedrive_sharelink/types.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\n// FolderResp represents the structure of the folder response from the OneDrive API.\ntype FolderResp struct {\n\t// Data holds the nested structure of the response.\n\tData struct {\n\t\tLegacy struct {\n\t\t\tRenderListData struct {\n\t\t\t\tListData struct {\n\t\t\t\t\tItems []Item `json:\"Row\"` // Items contains the list of items in the folder.\n\t\t\t\t} `json:\"ListData\"`\n\t\t\t} `json:\"renderListDataAsStream\"`\n\t\t} `json:\"legacy\"`\n\t} `json:\"data\"`\n}\n\n// Item represents an individual item in the folder.\ntype Item struct {\n\tObjType      string    `json:\"FSObjType\"`       // ObjType indicates if the item is a file or folder.\n\tName         string    `json:\"FileLeafRef\"`     // Name is the name of the item.\n\tModifiedTime time.Time `json:\"Modified.\"`       // ModifiedTime is the last modified time of the item.\n\tSize         string    `json:\"File_x0020_Size\"` // Size is the size of the item in string format.\n\tId           string    `json:\"UniqueId\"`        // Id is the unique identifier of the item.\n}\n\n// fileToObj converts an Item to an ObjThumb.\nfunc fileToObj(f Item) *model.ObjThumb {\n\t// Convert Size from string to int64.\n\tsize, _ := strconv.ParseInt(f.Size, 10, 64)\n\t// Convert ObjType from string to int.\n\tobjtype, _ := strconv.Atoi(f.ObjType)\n\n\t// Create a new ObjThumb with the converted values.\n\tfile := &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tName:     f.Name,\n\t\t\tModified: f.ModifiedTime,\n\t\t\tSize:     size,\n\t\t\tIsFolder: objtype == 1, // Check if the item is a folder.\n\t\t\tID:       f.Id,\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}\n\treturn file\n}\n\n// GraphQLNEWRequest represents the structure of a new GraphQL request.\ntype GraphQLNEWRequest struct {\n\tListData struct {\n\t\tNextHref string `json:\"NextHref\"` // NextHref is the link to the next set of data.\n\t\tRow      []Item `json:\"Row\"`      // Row contains the list of items.\n\t} `json:\"ListData\"`\n}\n\n// GraphQLRequest represents the structure of a GraphQL request.\ntype GraphQLRequest struct {\n\tData struct {\n\t\tLegacy struct {\n\t\t\tRenderListDataAsStream struct {\n\t\t\t\tListData struct {\n\t\t\t\t\tNextHref string `json:\"NextHref\"` // NextHref is the link to the next set of data.\n\t\t\t\t\tRow      []Item `json:\"Row\"`      // Row contains the list of items.\n\t\t\t\t} `json:\"ListData\"`\n\t\t\t\tViewMetadata struct {\n\t\t\t\t\tListViewXml string `json:\"ListViewXml\"` // ListViewXml contains the XML of the list view.\n\t\t\t\t} `json:\"ViewMetadata\"`\n\t\t\t} `json:\"renderListDataAsStream\"`\n\t\t} `json:\"legacy\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/onedrive_sharelink/util.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/net/html\"\n)\n\n// NewNoRedirectClient creates an HTTP client that doesn't follow redirects\nfunc NewNoRedirectCLient() *http.Client {\n\treturn &http.Client{\n\t\tTimeout: time.Hour * 48,\n\t\tTransport: &http.Transport{\n\t\t\tProxy:           http.ProxyFromEnvironment,\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},\n\t\t},\n\t\t// Prevent following redirects\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n}\n\n// getCookiesWithPassword fetches cookies required for authenticated access using the provided password\nfunc getCookiesWithPassword(link, password string) (string, error) {\n\t// Send GET request\n\tresp, err := http.Get(link)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Parse the HTML response\n\tdoc, err := html.Parse(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Initialize variables to store form data\n\tvar viewstate, eventvalidation, postAction string\n\n\t// Recursive function to find input fields by their IDs\n\tvar findInputFields func(*html.Node)\n\tfindInputFields = func(n *html.Node) {\n\t\tif n.Type == html.ElementNode && n.Data == \"input\" {\n\t\t\tfor _, attr := range n.Attr {\n\t\t\t\tif attr.Key == \"id\" {\n\t\t\t\t\tswitch attr.Val {\n\t\t\t\t\tcase \"__VIEWSTATE\":\n\t\t\t\t\t\tviewstate = getAttrValue(n, \"value\")\n\t\t\t\t\tcase \"__EVENTVALIDATION\":\n\t\t\t\t\t\teventvalidation = getAttrValue(n, \"value\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif n.Type == html.ElementNode && n.Data == \"form\" {\n\t\t\tfor _, attr := range n.Attr {\n\t\t\t\tif attr.Key == \"id\" && attr.Val == \"inputForm\" {\n\t\t\t\t\tpostAction = getAttrValue(n, \"action\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\tfindInputFields(c)\n\t\t}\n\t}\n\tfindInputFields(doc)\n\n\t// Prepare the new URL for the POST request\n\tlinkParts, err := url.Parse(link)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tnewURL := fmt.Sprintf(\"%s://%s%s\", linkParts.Scheme, linkParts.Host, postAction)\n\n\t// Prepare the request body\n\tdata := url.Values{\n\t\t\"txtPassword\":          []string{password},\n\t\t\"__EVENTVALIDATION\":    []string{eventvalidation},\n\t\t\"__VIEWSTATE\":          []string{viewstate},\n\t\t\"__VIEWSTATEENCRYPTED\": []string{\"\"},\n\t}\n\n\tclient := &http.Client{\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n\t// Send the POST request, preventing redirects\n\tresp, err = client.PostForm(newURL, data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Extract the desired cookie value\n\tcookie := resp.Cookies()\n\tvar fedAuthCookie string\n\tfor _, c := range cookie {\n\t\tif c.Name == \"FedAuth\" {\n\t\t\tfedAuthCookie = c.Value\n\t\t\tbreak\n\t\t}\n\t}\n\tif fedAuthCookie == \"\" {\n\t\treturn \"\", fmt.Errorf(\"wrong password\")\n\t}\n\treturn fmt.Sprintf(\"FedAuth=%s;\", fedAuthCookie), nil\n}\n\n// getAttrValue retrieves the value of the specified attribute from an HTML node\nfunc getAttrValue(n *html.Node, key string) string {\n\tfor _, attr := range n.Attr {\n\t\tif attr.Key == key {\n\t\t\treturn attr.Val\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// getHeaders constructs and returns the necessary HTTP headers for accessing the OneDrive share link\nfunc (d *OnedriveSharelink) getHeaders(ctx context.Context) (http.Header, error) {\n\theader := http.Header{}\n\theader.Set(\"User-Agent\", base.UserAgent)\n\theader.Set(\"accept-language\", \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\")\n\n\t// Save current timestamp to d.HeaderTime\n\td.HeaderTime = time.Now().Unix()\n\n\tif d.ShareLinkPassword == \"\" {\n\t\t// Create a no-redirect client\n\t\tclientNoDirect := NewNoRedirectCLient()\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, d.ShareLinkURL, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Set headers for the request\n\t\treq.Header = header\n\t\tanswerNoRedirect, err := clientNoDirect.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tredirectUrl := answerNoRedirect.Header.Get(\"Location\")\n\t\tlog.Debugln(\"redirectUrl:\", redirectUrl)\n\t\tif redirectUrl == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"password protected link. Please provide password\")\n\t\t}\n\t\theader.Set(\"Cookie\", answerNoRedirect.Header.Get(\"Set-Cookie\"))\n\t\theader.Set(\"Referer\", redirectUrl)\n\n\t\t// Extract the host part of the redirect URL and set it as the authority\n\t\tu, err := url.Parse(redirectUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\theader.Set(\"authority\", u.Host)\n\t\treturn header, nil\n\t} else {\n\t\tcookie, err := getCookiesWithPassword(d.ShareLinkURL, d.ShareLinkPassword)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\theader.Set(\"Cookie\", cookie)\n\t\theader.Set(\"Referer\", d.ShareLinkURL)\n\t\theader.Set(\"authority\", strings.Split(strings.Split(d.ShareLinkURL, \"//\")[1], \"/\")[0])\n\t\treturn header, nil\n\t}\n}\n\n// getFiles retrieves the files from the OneDrive share link at the specified path\nfunc (d *OnedriveSharelink) getFiles(ctx context.Context, path string) ([]Item, error) {\n\tclientNoDirect := NewNoRedirectCLient()\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, d.ShareLinkURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\theader := req.Header\n\tredirectUrl := \"\"\n\tif d.ShareLinkPassword == \"\" {\n\t\theader.Set(\"User-Agent\", base.UserAgent)\n\t\theader.Set(\"accept-language\", \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\")\n\t\treq.Header = header\n\t\tanswerNoRedirect, err := clientNoDirect.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tredirectUrl = answerNoRedirect.Header.Get(\"Location\")\n\t} else {\n\t\theader = d.Headers\n\t\treq.Header = header\n\t\tanswerNoRedirect, err := clientNoDirect.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tredirectUrl = answerNoRedirect.Header.Get(\"Location\")\n\t}\n\tredirectSplitURL := strings.Split(redirectUrl, \"/\")\n\treq.Header = d.Headers\n\tdownloadLinkPrefix := \"\"\n\trootFolderPre := \"\"\n\n\t// Determine the appropriate URL and root folder based on whether the link is SharePoint\n\tif d.IsSharepoint {\n\t\t// update req url\n\t\treq.URL, err = url.Parse(redirectUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Get redirectUrl\n\t\tanswer, err := clientNoDirect.Do(req)\n\t\tif err != nil {\n\t\t\td.Headers, err = d.getHeaders(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.getFiles(ctx, path)\n\t\t}\n\t\tdefer answer.Body.Close()\n\t\tre := regexp.MustCompile(`templateUrl\":\"(.*?)\"`)\n\t\tbody, err := io.ReadAll(answer.Body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttemplate := re.FindString(string(body))\n\t\ttemplate = template[strings.Index(template, \"templateUrl\\\":\\\"\")+len(\"templateUrl\\\":\\\"\"):]\n\t\ttemplate = template[:strings.Index(template, \"?id=\")]\n\t\ttemplate = template[:strings.LastIndex(template, \"/\")]\n\t\tdownloadLinkPrefix = template + \"/download.aspx?UniqueId=\"\n\t\tparams, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, \"?\")+1:])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trootFolderPre = params.Get(\"id\")\n\t} else {\n\t\tredirectUrlCut := redirectUrl[:strings.LastIndex(redirectUrl, \"/\")]\n\t\tdownloadLinkPrefix = redirectUrlCut + \"/download.aspx?UniqueId=\"\n\t\tparams, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, \"?\")+1:])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trootFolderPre = params.Get(\"id\")\n\t}\n\td.downloadLinkPrefix = downloadLinkPrefix\n\trootFolder, err := url.QueryUnescape(rootFolderPre)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugln(\"rootFolder:\", rootFolder)\n\t// Extract the relative path up to and including \"Documents\"\n\trelativePath := strings.Split(rootFolder, \"Documents\")[0] + \"Documents\"\n\n\t// URL encode the relative path\n\trelativeUrl := url.QueryEscape(relativePath)\n\t// Replace underscores and hyphens in the encoded relative path\n\trelativeUrl = strings.Replace(relativeUrl, \"_\", \"%5F\", -1)\n\trelativeUrl = strings.Replace(relativeUrl, \"-\", \"%2D\", -1)\n\n\t// If the path is not the root, append the path to the root folder\n\tif path != \"/\" {\n\t\trootFolder = rootFolder + path\n\t}\n\n\t// URL encode the full root folder path\n\trootFolderUrl := url.QueryEscape(rootFolder)\n\t// Replace underscores and hyphens in the encoded root folder URL\n\trootFolderUrl = strings.Replace(rootFolderUrl, \"_\", \"%5F\", -1)\n\trootFolderUrl = strings.Replace(rootFolderUrl, \"-\", \"%2D\", -1)\n\n\tlog.Debugln(\"relativePath:\", relativePath, \"relativeUrl:\", relativeUrl, \"rootFolder:\", rootFolder, \"rootFolderUrl:\", rootFolderUrl)\n\n\t// Construct the GraphQL query with the encoded paths\n\tgraphqlVar := fmt.Sprintf(`{\"query\":\"query (\\n        $listServerRelativeUrl: String!,$renderListDataAsStreamParameters: RenderListDataAsStreamParameters!,$renderListDataAsStreamQueryString: String!\\n        )\\n      {\\n      \\n      legacy {\\n      \\n      renderListDataAsStream(\\n      listServerRelativeUrl: $listServerRelativeUrl,\\n      parameters: $renderListDataAsStreamParameters,\\n      queryString: $renderListDataAsStreamQueryString\\n      )\\n    }\\n      \\n      \\n  perf {\\n    executionTime\\n    overheadTime\\n    parsingTime\\n    queryCount\\n    validationTime\\n    resolvers {\\n      name\\n      queryCount\\n      resolveTime\\n      waitTime\\n    }\\n  }\\n    }\",\"variables\":{\"listServerRelativeUrl\":\"%s\",\"renderListDataAsStreamParameters\":{\"renderOptions\":5707527,\"allowMultipleValueFilterForTaxonomyFields\":true,\"addRequiredFields\":true,\"folderServerRelativeUrl\":\"%s\"},\"renderListDataAsStreamQueryString\":\"@a1=\\'%s\\'&RootFolder=%s&TryNewExperienceSingle=TRUE\"}}`, relativePath, rootFolder, relativeUrl, rootFolderUrl)\n\ttempHeader := make(http.Header)\n\tfor k, v := range d.Headers {\n\t\ttempHeader[k] = v\n\t}\n\ttempHeader[\"Content-Type\"] = []string{\"application/json;odata=verbose\"}\n\n\tclient := &http.Client{}\n\tpostUrl := strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], \"/\") + \"/_api/v2.1/graphql\"\n\treq, err = http.NewRequest(http.MethodPost, postUrl, strings.NewReader(graphqlVar))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header = tempHeader\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\td.Headers, err = d.getHeaders(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn d.getFiles(ctx, path)\n\t}\n\tdefer resp.Body.Close()\n\tvar graphqlReq GraphQLRequest\n\tjson.NewDecoder(resp.Body).Decode(&graphqlReq)\n\tlog.Debugln(\"graphqlReq:\", graphqlReq)\n\tfilesData := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row\n\tif graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref != \"\" {\n\t\tnextHref := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref + \"&@a1=REPLACEME&TryNewExperienceSingle=TRUE\"\n\t\tnextHref = strings.Replace(nextHref, \"REPLACEME\", \"%27\"+relativeUrl+\"%27\", -1)\n\t\tlog.Debugln(\"nextHref:\", nextHref)\n\t\tfilesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)\n\n\t\tlistViewXml := graphqlReq.Data.Legacy.RenderListDataAsStream.ViewMetadata.ListViewXml\n\t\tlog.Debugln(\"listViewXml:\", listViewXml)\n\t\trenderListDataAsStreamVar := `{\"parameters\":{\"__metadata\":{\"type\":\"SP.RenderListDataParameters\"},\"RenderOptions\":1216519,\"ViewXml\":\"REPLACEME\",\"AllowMultipleValueFilterForTaxonomyFields\":true,\"AddRequiredFields\":true}}`\n\t\tlistViewXml = strings.Replace(listViewXml, `\"`, `\\\"`, -1)\n\t\trenderListDataAsStreamVar = strings.Replace(renderListDataAsStreamVar, \"REPLACEME\", listViewXml, -1)\n\n\t\tgraphqlReqNEW := GraphQLNEWRequest{}\n\t\tpostUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], \"/\") + \"/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream\" + nextHref\n\t\treq, _ = http.NewRequest(http.MethodPost, postUrl, strings.NewReader(renderListDataAsStreamVar))\n\t\treq.Header = tempHeader\n\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\td.Headers, err = d.getHeaders(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.getFiles(ctx, path)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tjson.NewDecoder(resp.Body).Decode(&graphqlReqNEW)\n\t\tfor graphqlReqNEW.ListData.NextHref != \"\" {\n\t\t\tgraphqlReqNEW = GraphQLNEWRequest{}\n\t\t\tpostUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], \"/\") + \"/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream\" + nextHref\n\t\t\treq, _ = http.NewRequest(http.MethodPost, postUrl, strings.NewReader(renderListDataAsStreamVar))\n\t\t\treq.Header = tempHeader\n\t\t\tresp, err := client.Do(req)\n\t\t\tif err != nil {\n\t\t\t\td.Headers, err = d.getHeaders(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn d.getFiles(ctx, path)\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tjson.NewDecoder(resp.Body).Decode(&graphqlReqNEW)\n\t\t\tnextHref = graphqlReqNEW.ListData.NextHref + \"&@a1=REPLACEME&TryNewExperienceSingle=TRUE\"\n\t\t\tnextHref = strings.Replace(nextHref, \"REPLACEME\", \"%27\"+relativeUrl+\"%27\", -1)\n\t\t\tfilesData = append(filesData, graphqlReqNEW.ListData.Row...)\n\t\t}\n\t\tfilesData = append(filesData, graphqlReqNEW.ListData.Row...)\n\t} else {\n\t\tfilesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)\n\t}\n\treturn filesData, nil\n}\n"
  },
  {
    "path": "drivers/openlist/driver.go",
    "content": "package openlist\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype OpenList struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *OpenList) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *OpenList) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *OpenList) Init(ctx context.Context) error {\n\td.Addition.Address = strings.TrimSuffix(d.Addition.Address, \"/\")\n\tvar resp common.Resp[MeResp]\n\t_, _, err := d.request(\"/me\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\t// if the username is not empty and the username is not the same as the current username, then login again\n\tif d.Username != resp.Data.Username {\n\t\terr = d.login()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// re-get the user info\n\t_, _, err = d.request(\"/me\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Data.Role == model.GUEST {\n\t\tu := d.Address + \"/api/public/settings\"\n\t\tres, err := base.RestyClient.R().Get(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tallowMounted := utils.Json.Get(res.Body(), \"data\", conf.AllowMounted).ToString() == \"true\"\n\t\tif !allowMounted {\n\t\t\treturn fmt.Errorf(\"the site does not allow mounted\")\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *OpenList) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *OpenList) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar resp common.Resp[FsListResp]\n\t_, _, err := d.request(\"/fs/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ListReq{\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tPath:     dir.GetPath(),\n\t\t\tPassword: d.MetaPassword,\n\t\t\tRefresh:  false,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tPath:     path.Join(dir.GetPath(), f.Name),\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *OpenList) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp common.Resp[FsGetResp]\n\theaders := map[string]string{\n\t\t\"User-Agent\": base.UserAgent,\n\t}\n\t// if PassUAToUpsteam is true, then pass the user-agent to the upstream\n\tif d.PassUAToUpsteam {\n\t\tuserAgent := args.Header.Get(\"user-agent\")\n\t\tif userAgent != \"\" {\n\t\t\theaders[\"User-Agent\"] = userAgent\n\t\t}\n\t}\n\t// if PassIPToUpsteam is true, then pass the ip address to the upstream\n\tif d.PassIPToUpsteam {\n\t\tip := args.IP\n\t\tif ip != \"\" {\n\t\t\theaders[\"X-Forwarded-For\"] = ip\n\t\t\theaders[\"X-Real-Ip\"] = ip\n\t\t}\n\t}\n\t_, _, err := d.request(\"/fs/get\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(FsGetReq{\n\t\t\tPath:     file.GetPath(),\n\t\t\tPassword: d.MetaPassword,\n\t\t}).SetHeaders(headers)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: resp.Data.RawURL,\n\t}, nil\n}\n\nfunc (d *OpenList) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, _, err := d.request(\"/fs/mkdir\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MkdirOrLinkReq{\n\t\t\tPath: path.Join(parentDir.GetPath(), dirName),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, _, err := d.request(\"/fs/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MoveCopyReq{\n\t\t\tSrcDir: path.Dir(srcObj.GetPath()),\n\t\t\tDstDir: dstDir.GetPath(),\n\t\t\tNames:  []string{srcObj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, _, err := d.request(\"/fs/rename\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(RenameReq{\n\t\t\tPath: srcObj.GetPath(),\n\t\t\tName: newName,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, _, err := d.request(\"/fs/copy\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(MoveCopyReq{\n\t\t\tSrcDir: path.Dir(srcObj.GetPath()),\n\t\t\tDstDir: dstDir.GetPath(),\n\t\t\tNames:  []string{srcObj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) Remove(ctx context.Context, obj model.Obj) error {\n\t_, _, err := d.request(\"/fs/remove\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(RemoveReq{\n\t\t\tDir:   path.Dir(obj.GetPath()),\n\t\t\tNames: []string{obj.GetName()},\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+\"/api/fs/put\", reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Authorization\", d.Token)\n\treq.Header.Set(\"File-Path\", path.Join(dstDir.GetPath(), s.GetName()))\n\treq.Header.Set(\"Password\", d.MetaPassword)\n\tif md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 {\n\t\treq.Header.Set(\"X-File-Md5\", md5)\n\t}\n\tif sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 {\n\t\treq.Header.Set(\"X-File-Sha1\", sha1)\n\t}\n\tif sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 {\n\t\treq.Header.Set(\"X-File-Sha256\", sha256)\n\t}\n\n\treq.ContentLength = s.GetSize()\n\t// client := base.NewHttpClient()\n\t// client.Timeout = time.Hour * 6\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbytes, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"[openlist] response body: %s\", string(bytes))\n\tif res.StatusCode >= 400 {\n\t\treturn fmt.Errorf(\"request failed, status: %s\", res.Status)\n\t}\n\tcode := utils.Json.Get(bytes, \"code\").ToInt()\n\tif code != 200 {\n\t\tif code == 401 || code == 403 {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"request failed,code: %d, message: %s\", code, utils.Json.Get(bytes, \"message\").ToString())\n\t}\n\treturn nil\n}\n\nfunc (d *OpenList) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveMetaResp]\n\t_, code, err := d.request(\"/fs/archive/meta\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveMetaReq{\n\t\t\tArchivePass: args.Password,\n\t\t\tPassword:    d.MetaPassword,\n\t\t\tPath:        obj.GetPath(),\n\t\t\tRefresh:     false,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar tree []model.ObjTree\n\tif resp.Data.Content != nil {\n\t\ttree = make([]model.ObjTree, 0, len(resp.Data.Content))\n\t\tfor _, content := range resp.Data.Content {\n\t\t\ttree = append(tree, &content)\n\t\t}\n\t}\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   resp.Data.Comment,\n\t\tEncrypted: resp.Data.Encrypted,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (d *OpenList) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveListResp]\n\t_, code, err := d.request(\"/fs/archive/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveListReq{\n\t\t\tArchiveMetaReq: ArchiveMetaReq{\n\t\t\t\tArchivePass: args.Password,\n\t\t\t\tPassword:    d.MetaPassword,\n\t\t\t\tPath:        obj.GetPath(),\n\t\t\t\tRefresh:     false,\n\t\t\t},\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tInnerPath: args.InnerPath,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *OpenList) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\tif !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotSupport\n\t}\n\tvar resp common.Resp[ArchiveMetaResp]\n\t_, _, err := d.request(\"/fs/archive/meta\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveMetaReq{\n\t\t\tArchivePass: args.Password,\n\t\t\tPassword:    d.MetaPassword,\n\t\t\tPath:        obj.GetPath(),\n\t\t\tRefresh:     false,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: fmt.Sprintf(\"%s?inner=%s&pass=%s&sign=%s\",\n\t\t\tresp.Data.RawURL,\n\t\t\tutils.EncodePath(args.InnerPath, true),\n\t\t\turl.QueryEscape(args.Password),\n\t\t\tresp.Data.Sign),\n\t}, nil\n}\n\nfunc (d *OpenList) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {\n\tif !d.ForwardArchiveReq {\n\t\treturn errs.NotImplement\n\t}\n\tdir, name := path.Split(srcObj.GetPath())\n\t_, _, err := d.request(\"/fs/archive/decompress\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(DecompressReq{\n\t\t\tArchivePass:   args.Password,\n\t\t\tCacheFull:     args.CacheFull,\n\t\t\tDstDir:        dstDir.GetPath(),\n\t\t\tInnerPath:     args.InnerPath,\n\t\t\tName:          []string{name},\n\t\t\tPutIntoNewDir: args.PutIntoNewDir,\n\t\t\tSrcDir:        dir,\n\t\t\tOverwrite:     args.Overwrite,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *OpenList) ResolveLinkCacheMode(_ string) driver.LinkCacheMode {\n\tvar mode driver.LinkCacheMode\n\tif d.PassIPToUpsteam {\n\t\tmode |= driver.LinkCacheIP\n\t}\n\tif d.PassUAToUpsteam {\n\t\tmode |= driver.LinkCacheUA\n\t}\n\treturn mode\n}\n\nvar _ driver.Driver = (*OpenList)(nil)\n"
  },
  {
    "path": "drivers/openlist/meta.go",
    "content": "package openlist\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress           string `json:\"url\" required:\"true\"`\n\tMetaPassword      string `json:\"meta_password\"`\n\tUsername          string `json:\"username\"`\n\tPassword          string `json:\"password\"`\n\tToken             string `json:\"token\"`\n\tPassIPToUpsteam   bool   `json:\"pass_ip_to_upsteam\" default:\"true\"`\n\tPassUAToUpsteam   bool   `json:\"pass_ua_to_upsteam\" default:\"true\"`\n\tForwardArchiveReq bool   `json:\"forward_archive_requests\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:             \"OpenList\",\n\tLocalSort:        true,\n\tDefaultRoot:      \"/\",\n\tProxyRangeOption: true,\n\tLinkCacheMode:    driver.LinkCacheAuto,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &OpenList{}\n\t})\n}\n"
  },
  {
    "path": "drivers/openlist/types.go",
    "content": "package openlist\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ListReq struct {\n\tmodel.PageReq\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n\tRefresh  bool   `json:\"refresh\"`\n}\n\ntype ObjResp struct {\n\tName     string    `json:\"name\"`\n\tSize     int64     `json:\"size\"`\n\tIsDir    bool      `json:\"is_dir\"`\n\tModified time.Time `json:\"modified\"`\n\tCreated  time.Time `json:\"created\"`\n\tSign     string    `json:\"sign\"`\n\tThumb    string    `json:\"thumb\"`\n\tType     int       `json:\"type\"`\n\tHashInfo string    `json:\"hashinfo\"`\n}\n\ntype FsListResp struct {\n\tContent  []ObjResp `json:\"content\"`\n\tTotal    int64     `json:\"total\"`\n\tReadme   string    `json:\"readme\"`\n\tWrite    bool      `json:\"write\"`\n\tProvider string    `json:\"provider\"`\n}\n\ntype FsGetReq struct {\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n}\n\ntype FsGetResp struct {\n\tObjResp\n\tRawURL   string    `json:\"raw_url\"`\n\tReadme   string    `json:\"readme\"`\n\tProvider string    `json:\"provider\"`\n\tRelated  []ObjResp `json:\"related\"`\n}\n\ntype MkdirOrLinkReq struct {\n\tPath string `json:\"path\" form:\"path\"`\n}\n\ntype MoveCopyReq struct {\n\tSrcDir string   `json:\"src_dir\"`\n\tDstDir string   `json:\"dst_dir\"`\n\tNames  []string `json:\"names\"`\n}\n\ntype RenameReq struct {\n\tPath string `json:\"path\"`\n\tName string `json:\"name\"`\n}\n\ntype RemoveReq struct {\n\tDir   string   `json:\"dir\"`\n\tNames []string `json:\"names\"`\n}\n\ntype LoginResp struct {\n\tToken string `json:\"token\"`\n}\n\ntype MeResp struct {\n\tId         int    `json:\"id\"`\n\tUsername   string `json:\"username\"`\n\tPassword   string `json:\"password\"`\n\tBasePath   string `json:\"base_path\"`\n\tRole       int    `json:\"role\"`\n\tDisabled   bool   `json:\"disabled\"`\n\tPermission int    `json:\"permission\"`\n\tSsoId      string `json:\"sso_id\"`\n\tOtp        bool   `json:\"otp\"`\n}\n\ntype ArchiveMetaReq struct {\n\tArchivePass string `json:\"archive_pass\"`\n\tPassword    string `json:\"password\"`\n\tPath        string `json:\"path\"`\n\tRefresh     bool   `json:\"refresh\"`\n}\n\ntype TreeResp struct {\n\tObjResp\n\tChildren  []TreeResp `json:\"children\"`\n\thashCache *utils.HashInfo\n}\n\nfunc (t *TreeResp) GetSize() int64 {\n\treturn t.Size\n}\n\nfunc (t *TreeResp) GetName() string {\n\treturn t.Name\n}\n\nfunc (t *TreeResp) ModTime() time.Time {\n\treturn t.Modified\n}\n\nfunc (t *TreeResp) CreateTime() time.Time {\n\treturn t.Created\n}\n\nfunc (t *TreeResp) IsDir() bool {\n\treturn t.ObjResp.IsDir\n}\n\nfunc (t *TreeResp) GetHash() utils.HashInfo {\n\treturn utils.FromString(t.HashInfo)\n}\n\nfunc (t *TreeResp) GetID() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetPath() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetChildren() []model.ObjTree {\n\tret := make([]model.ObjTree, 0, len(t.Children))\n\tfor _, child := range t.Children {\n\t\tret = append(ret, &child)\n\t}\n\treturn ret\n}\n\nfunc (t *TreeResp) Thumb() string {\n\treturn t.ObjResp.Thumb\n}\n\ntype ArchiveMetaResp struct {\n\tComment   string     `json:\"comment\"`\n\tEncrypted bool       `json:\"encrypted\"`\n\tContent   []TreeResp `json:\"content\"`\n\tRawURL    string     `json:\"raw_url\"`\n\tSign      string     `json:\"sign\"`\n}\n\ntype ArchiveListReq struct {\n\tmodel.PageReq\n\tArchiveMetaReq\n\tInnerPath string `json:\"inner_path\"`\n}\n\ntype ArchiveListResp struct {\n\tContent []ObjResp `json:\"content\"`\n\tTotal   int64     `json:\"total\"`\n}\n\ntype DecompressReq struct {\n\tArchivePass   string   `json:\"archive_pass\"`\n\tCacheFull     bool     `json:\"cache_full\"`\n\tDstDir        string   `json:\"dst_dir\"`\n\tInnerPath     string   `json:\"inner_path\"`\n\tName          []string `json:\"name\"`\n\tPutIntoNewDir bool     `json:\"put_into_new_dir\"`\n\tSrcDir        string   `json:\"src_dir\"`\n\tOverwrite     bool     `json:\"overwrite\"`\n}\n"
  },
  {
    "path": "drivers/openlist/util.go",
    "content": "package openlist\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *OpenList) login() error {\n\tif d.Username == \"\" {\n\t\treturn nil\n\t}\n\tvar resp common.Resp[LoginResp]\n\t_, _, err := d.request(\"/auth/login\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(base.Json{\n\t\t\t\"username\": d.Username,\n\t\t\t\"password\": d.Password,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Token = resp.Data.Token\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *OpenList) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) {\n\turl := d.Address + \"/api\" + api\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", d.Token)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\tcode := 0\n\t\tif res != nil {\n\t\t\tcode = res.StatusCode()\n\t\t}\n\t\treturn nil, code, err\n\t}\n\tlog.Debugf(\"[openlist] response body: %s\", res.String())\n\tif res.StatusCode() >= 400 {\n\t\treturn nil, res.StatusCode(), fmt.Errorf(\"request failed, status: %s\", res.Status())\n\t}\n\tcode := utils.Json.Get(res.Body(), \"code\").ToInt()\n\tif code != 200 {\n\t\tif (code == 401 || code == 403) && !utils.IsBool(retry...) {\n\t\t\terr = d.login()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, code, err\n\t\t\t}\n\t\t\treturn d.request(api, method, callback, true)\n\t\t}\n\t\treturn nil, code, fmt.Errorf(\"request failed,code: %d, message: %s\", code, utils.Json.Get(res.Body(), \"message\").ToString())\n\t}\n\treturn res.Body(), 200, nil\n}\n"
  },
  {
    "path": "drivers/openlist_share/driver.go",
    "content": "package openlist_share\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype OpenListShare struct {\n\tmodel.Storage\n\tAddition\n\tserverArchivePreview bool\n}\n\nfunc (d *OpenListShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *OpenListShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *OpenListShare) Init(ctx context.Context) error {\n\td.Addition.Address = strings.TrimSuffix(d.Addition.Address, \"/\")\n\tvar settings common.Resp[map[string]string]\n\t_, _, err := d.request(\"/public/settings\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetResult(&settings)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\td.serverArchivePreview = settings.Data[\"share_archive_preview\"] == \"true\"\n\treturn nil\n}\n\nfunc (d *OpenListShare) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *OpenListShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar resp common.Resp[FsListResp]\n\t_, _, err := d.request(\"/fs/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ListReq{\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tPath:     stdpath.Join(fmt.Sprintf(\"/@s/%s\", d.ShareId), dir.GetPath()),\n\t\t\tPassword: d.Pwd,\n\t\t\tRefresh:  false,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tPath:     stdpath.Join(dir.GetPath(), f.Name),\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *OpenListShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tpath := utils.FixAndCleanPath(stdpath.Join(d.ShareId, file.GetPath()))\n\tu := fmt.Sprintf(\"%s/sd%s?pwd=%s\", d.Address, path, d.Pwd)\n\treturn &model.Link{URL: u}, nil\n}\n\nfunc (d *OpenListShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tif !d.serverArchivePreview || !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveMetaResp]\n\t_, code, err := d.request(\"/fs/archive/meta\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveMetaReq{\n\t\t\tArchivePass: args.Password,\n\t\t\tPath:        stdpath.Join(fmt.Sprintf(\"/@s/%s\", d.ShareId), obj.GetPath()),\n\t\t\tPassword:    d.Pwd,\n\t\t\tRefresh:     false,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar tree []model.ObjTree\n\tif resp.Data.Content != nil {\n\t\ttree = make([]model.ObjTree, 0, len(resp.Data.Content))\n\t\tfor _, content := range resp.Data.Content {\n\t\t\ttree = append(tree, &content)\n\t\t}\n\t}\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   resp.Data.Comment,\n\t\tEncrypted: resp.Data.Encrypted,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (d *OpenListShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tif !d.serverArchivePreview || !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotImplement\n\t}\n\tvar resp common.Resp[ArchiveListResp]\n\t_, code, err := d.request(\"/fs/archive/list\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetBody(ArchiveListReq{\n\t\t\tArchiveMetaReq: ArchiveMetaReq{\n\t\t\t\tArchivePass: args.Password,\n\t\t\t\tPath:        stdpath.Join(fmt.Sprintf(\"/@s/%s\", d.ShareId), obj.GetPath()),\n\t\t\t\tPassword:    d.Pwd,\n\t\t\t\tRefresh:     false,\n\t\t\t},\n\t\t\tPageReq: model.PageReq{\n\t\t\t\tPage:    1,\n\t\t\t\tPerPage: 0,\n\t\t\t},\n\t\t\tInnerPath: args.InnerPath,\n\t\t})\n\t})\n\tif code == 202 {\n\t\treturn nil, errs.WrongArchivePassword\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Content {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: f.Modified,\n\t\t\t\tCtime:    f.Created,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.IsDir,\n\t\t\t\tHashInfo: utils.FromString(f.HashInfo),\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *OpenListShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\tif !d.serverArchivePreview || !d.ForwardArchiveReq {\n\t\treturn nil, errs.NotSupport\n\t}\n\tpath := utils.FixAndCleanPath(stdpath.Join(d.ShareId, obj.GetPath()))\n\tu := fmt.Sprintf(\"%s/sad%s?pwd=%s&inner=%s&pass=%s\",\n\t\td.Address,\n\t\tpath,\n\t\td.Pwd,\n\t\tutils.EncodePath(args.InnerPath, true),\n\t\turl.QueryEscape(args.Password))\n\treturn &model.Link{URL: u}, nil\n}\n\nvar _ driver.Driver = (*OpenListShare)(nil)\n"
  },
  {
    "path": "drivers/openlist_share/meta.go",
    "content": "package openlist_share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress           string `json:\"url\" required:\"true\"`\n\tShareId           string `json:\"sid\" required:\"true\"`\n\tPwd               string `json:\"pwd\"`\n\tForwardArchiveReq bool   `json:\"forward_archive_requests\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:        \"OpenListShare\",\n\tLocalSort:   true,\n\tNoUpload:    true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &OpenListShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/openlist_share/types.go",
    "content": "package openlist_share\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ListReq struct {\n\tmodel.PageReq\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n\tRefresh  bool   `json:\"refresh\"`\n}\n\ntype ObjResp struct {\n\tName     string    `json:\"name\"`\n\tSize     int64     `json:\"size\"`\n\tIsDir    bool      `json:\"is_dir\"`\n\tModified time.Time `json:\"modified\"`\n\tCreated  time.Time `json:\"created\"`\n\tSign     string    `json:\"sign\"`\n\tThumb    string    `json:\"thumb\"`\n\tType     int       `json:\"type\"`\n\tHashInfo string    `json:\"hashinfo\"`\n}\n\ntype FsListResp struct {\n\tContent  []ObjResp `json:\"content\"`\n\tTotal    int64     `json:\"total\"`\n\tReadme   string    `json:\"readme\"`\n\tWrite    bool      `json:\"write\"`\n\tProvider string    `json:\"provider\"`\n}\n\ntype ArchiveMetaReq struct {\n\tArchivePass string `json:\"archive_pass\"`\n\tPassword    string `json:\"password\"`\n\tPath        string `json:\"path\"`\n\tRefresh     bool   `json:\"refresh\"`\n}\n\ntype TreeResp struct {\n\tObjResp\n\tChildren  []TreeResp `json:\"children\"`\n\thashCache *utils.HashInfo\n}\n\nfunc (t *TreeResp) GetSize() int64 {\n\treturn t.Size\n}\n\nfunc (t *TreeResp) GetName() string {\n\treturn t.Name\n}\n\nfunc (t *TreeResp) ModTime() time.Time {\n\treturn t.Modified\n}\n\nfunc (t *TreeResp) CreateTime() time.Time {\n\treturn t.Created\n}\n\nfunc (t *TreeResp) IsDir() bool {\n\treturn t.ObjResp.IsDir\n}\n\nfunc (t *TreeResp) GetHash() utils.HashInfo {\n\treturn utils.FromString(t.HashInfo)\n}\n\nfunc (t *TreeResp) GetID() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetPath() string {\n\treturn \"\"\n}\n\nfunc (t *TreeResp) GetChildren() []model.ObjTree {\n\tret := make([]model.ObjTree, 0, len(t.Children))\n\tfor _, child := range t.Children {\n\t\tret = append(ret, &child)\n\t}\n\treturn ret\n}\n\nfunc (t *TreeResp) Thumb() string {\n\treturn t.ObjResp.Thumb\n}\n\ntype ArchiveMetaResp struct {\n\tComment   string     `json:\"comment\"`\n\tEncrypted bool       `json:\"encrypted\"`\n\tContent   []TreeResp `json:\"content\"`\n\tRawURL    string     `json:\"raw_url\"`\n\tSign      string     `json:\"sign\"`\n}\n\ntype ArchiveListReq struct {\n\tmodel.PageReq\n\tArchiveMetaReq\n\tInnerPath string `json:\"inner_path\"`\n}\n\ntype ArchiveListResp struct {\n\tContent []ObjResp `json:\"content\"`\n\tTotal   int64     `json:\"total\"`\n}\n"
  },
  {
    "path": "drivers/openlist_share/util.go",
    "content": "package openlist_share\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc (d *OpenListShare) request(api, method string, callback base.ReqCallback) ([]byte, int, error) {\n\turl := d.Address + \"/api\" + api\n\treq := base.RestyClient.R()\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\tcode := 0\n\t\tif res != nil {\n\t\t\tcode = res.StatusCode()\n\t\t}\n\t\treturn nil, code, err\n\t}\n\tif res.StatusCode() >= 400 {\n\t\treturn nil, res.StatusCode(), fmt.Errorf(\"request failed, status: %s\", res.Status())\n\t}\n\tcode := utils.Json.Get(res.Body(), \"code\").ToInt()\n\tif code != 200 {\n\t\treturn nil, code, fmt.Errorf(\"request failed, code: %d, message: %s\", code, utils.Json.Get(res.Body(), \"message\").ToString())\n\t}\n\treturn res.Body(), 200, nil\n}\n"
  },
  {
    "path": "drivers/pikpak/driver.go",
    "content": "package pikpak\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype PikPak struct {\n\tmodel.Storage\n\tAddition\n\t*Common\n\tRefreshToken string\n\tAccessToken  string\n}\n\nfunc (d *PikPak) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *PikPak) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *PikPak) Init(ctx context.Context) (err error) {\n\tif d.Common == nil {\n\t\td.Common = &Common{\n\t\t\tclient:       base.NewRestyClient(),\n\t\t\tCaptchaToken: \"\",\n\t\t\tUserID:       \"\",\n\t\t\tDeviceID:     utils.GetMD5EncodeStr(d.Username + d.Password),\n\t\t\tUserAgent:    \"\",\n\t\t\tRefreshCTokenCk: func(token string) {\n\t\t\t\td.Common.CaptchaToken = token\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t},\n\t\t}\n\t}\n\n\tif d.Platform == \"android\" {\n\t\td.ClientID = AndroidClientID\n\t\td.ClientSecret = AndroidClientSecret\n\t\td.ClientVersion = AndroidClientVersion\n\t\td.PackageName = AndroidPackageName\n\t\td.Algorithms = AndroidAlgorithms\n\t\td.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, \"\")\n\t} else if d.Platform == \"web\" {\n\t\td.ClientID = WebClientID\n\t\td.ClientSecret = WebClientSecret\n\t\td.ClientVersion = WebClientVersion\n\t\td.PackageName = WebPackageName\n\t\td.Algorithms = WebAlgorithms\n\t\td.UserAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n\t} else if d.Platform == \"pc\" {\n\t\td.ClientID = PCClientID\n\t\td.ClientSecret = PCClientSecret\n\t\td.ClientVersion = PCClientVersion\n\t\td.PackageName = PCPackageName\n\t\td.Algorithms = PCAlgorithms\n\t\td.UserAgent = \"MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.6.11.4955 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36\"\n\t}\n\n\tif d.Addition.CaptchaToken != \"\" && d.Addition.RefreshToken == \"\" {\n\t\td.SetCaptchaToken(d.Addition.CaptchaToken)\n\t}\n\n\tif d.Addition.DeviceID != \"\" {\n\t\td.SetDeviceID(d.Addition.DeviceID)\n\t} else {\n\t\td.Addition.DeviceID = d.Common.DeviceID\n\t\top.MustSaveDriverStorage(d)\n\t}\n\t// 如果已经有RefreshToken，直接获取AccessToken\n\tif d.Addition.RefreshToken != \"\" {\n\t\tif err = d.refreshToken(d.Addition.RefreshToken); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// 如果没有填写RefreshToken，尝试登录 获取 refreshToken\n\t\tif err = d.login(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 获取CaptchaToken\n\terr = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, \"https://api-drive.mypikpak.net/drive/v1/files\"), d.Common.GetUserID())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 更新UserAgent\n\tif d.Platform == \"android\" {\n\t\td.Common.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, d.Common.UserID)\n\t}\n\n\t// 保存 有效的 RefreshToken\n\td.Addition.RefreshToken = d.RefreshToken\n\top.MustSaveDriverStorage(d)\n\n\treturn nil\n}\n\nfunc (d *PikPak) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp File\n\tvar url string\n\tqueryParams := map[string]string{\n\t\t\"_magic\":         \"2021\",\n\t\t\"usage\":          \"FETCH\",\n\t\t\"thumbnail_size\": \"SIZE_LARGE\",\n\t}\n\tif !d.DisableMediaLink {\n\t\tqueryParams[\"usage\"] = \"CACHE\"\n\t}\n\t_, err := d.request(fmt.Sprintf(\"https://api-drive.mypikpak.net/drive/v1/files/%s\", file.GetID()),\n\t\thttp.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx).\n\t\t\t\tSetQueryParams(queryParams)\n\t\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\turl = resp.WebContentLink\n\n\tif !d.DisableMediaLink && len(resp.Medias) > 0 && resp.Medias[0].Link.Url != \"\" {\n\t\tlog.Debugln(\"use media link\")\n\t\turl = resp.Medias[0].Link.Url\n\t}\n\n\treturn &model.Link{\n\t\tURL: url,\n\t}, nil\n}\n\nfunc (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"kind\":      \"drive#folder\",\n\t\t\t\"parent_id\": parentDir.GetID(),\n\t\t\t\"name\":      dirName,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files:batchMove\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"ids\": []string{srcObj.GetID()},\n\t\t\t\"to\": base.Json{\n\t\t\t\t\"parent_id\": dstDir.GetID(),\n\t\t\t},\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files/\"+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"name\": newName,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files:batchCopy\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"ids\": []string{srcObj.GetID()},\n\t\t\t\"to\": base.Json{\n\t\t\t\t\"parent_id\": dstDir.GetID(),\n\t\t\t},\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *PikPak) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files:batchTrash\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).SetBody(base.Json{\n\t\t\t\"ids\": []string{obj.GetID()},\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tsha1Str := stream.GetHash().GetHash(hash_extend.GCID)\n\n\tif len(sha1Str) < hash_extend.GCID.Width {\n\t\tvar err error\n\t\t_, sha1Str, err = streamPkg.CacheFullAndHash(stream, &up, hash_extend.GCID, stream.GetSize())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar resp UploadTaskData\n\tres, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"kind\":        \"drive#file\",\n\t\t\t\"name\":        stream.GetName(),\n\t\t\t\"size\":        stream.GetSize(),\n\t\t\t\"hash\":        strings.ToUpper(sha1Str),\n\t\t\t\"upload_type\": \"UPLOAD_TYPE_RESUMABLE\",\n\t\t\t\"objProvider\": base.Json{\"provider\": \"UPLOAD_TYPE_UNKNOWN\"},\n\t\t\t\"parent_id\":   dstDir.GetID(),\n\t\t\t\"folder_type\": \"NORMAL\",\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 秒传成功\n\tif resp.Resumable == nil {\n\t\tlog.Debugln(string(res))\n\t\treturn nil\n\t}\n\n\tparams := resp.Resumable.Params\n\t// endpoint := strings.Join(strings.Split(params.Endpoint, \".\")[1:], \".\")\n\t// web 端上传 返回的endpoint 为 `mypikpak.net` | android 端上传 返回的endpoint 为 `vip-lixian-07.mypikpak.net`·\n\tif d.Addition.Platform == \"android\" {\n\t\tparams.Endpoint = \"mypikpak.net\"\n\t}\n\n\tif stream.GetSize() <= 10*utils.MB { // 文件大小 小于10MB，改用普通模式上传\n\t\treturn d.UploadByOSS(ctx, &params, stream, up)\n\t}\n\t// 分片上传\n\treturn d.UploadByMultipart(ctx, &params, stream.GetSize(), stream, up)\n}\n\nfunc (d *PikPak) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tvar about AboutResponse\n\t_, err := d.request(\"https://api-drive.mypikpak.com/drive/v1/about\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t}, &about)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := strconv.ParseInt(about.Quota.Limit, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(about.Quota.Usage, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\n// 离线下载文件\nfunc (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {\n\trequestBody := base.Json{\n\t\t\"kind\":        \"drive#file\",\n\t\t\"name\":        fileName,\n\t\t\"upload_type\": \"UPLOAD_TYPE_URL\",\n\t\t\"url\": base.Json{\n\t\t\t\"url\": fileUrl,\n\t\t},\n\t\t\"parent_id\":   parentDir.GetID(),\n\t\t\"folder_type\": \"\",\n\t}\n\n\tvar resp OfflineDownloadResp\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetBody(requestBody)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Task, err\n}\n\n/*\n获取离线下载任务列表\nphase 可能的取值：\nPHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING\n*/\nfunc (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {\n\tres := make([]OfflineTask, 0)\n\turl := \"https://api-drive.mypikpak.net/drive/v1/tasks\"\n\n\tif len(phase) == 0 {\n\t\tphase = []string{\"PHASE_TYPE_RUNNING\", \"PHASE_TYPE_ERROR\", \"PHASE_TYPE_COMPLETE\", \"PHASE_TYPE_PENDING\"}\n\t}\n\tparams := map[string]string{\n\t\t\"type\":           \"offline\",\n\t\t\"thumbnail_size\": \"SIZE_SMALL\",\n\t\t\"limit\":          \"10000\",\n\t\t\"page_token\":     nextPageToken,\n\t\t\"with\":           \"reference_resource\",\n\t}\n\n\t// 处理 phase 参数\n\tif len(phase) > 0 {\n\t\tfilters := base.Json{\n\t\t\t\"phase\": map[string]string{\n\t\t\t\t\"in\": strings.Join(phase, \",\"),\n\t\t\t},\n\t\t}\n\t\tfiltersJSON, err := json.Marshal(filters)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to marshal filters: %w\", err)\n\t\t}\n\t\tparams[\"filters\"] = string(filtersJSON)\n\t}\n\n\tvar resp OfflineListResp\n\t_, err := d.request(url, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(params)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get offline list: %w\", err)\n\t}\n\tres = append(res, resp.Tasks...)\n\treturn res, nil\n}\n\nfunc (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {\n\turl := \"https://api-drive.mypikpak.net/drive/v1/tasks\"\n\tparams := map[string]string{\n\t\t\"task_ids\":     strings.Join(taskIDs, \",\"),\n\t\t\"delete_files\": strconv.FormatBool(deleteFiles),\n\t}\n\t_, err := d.request(url, http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(params)\n\t}, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete tasks %v: %w\", taskIDs, err)\n\t}\n\treturn nil\n}\n\nvar _ driver.Driver = (*PikPak)(nil)\n"
  },
  {
    "path": "drivers/pikpak/meta.go",
    "content": "package pikpak\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername         string `json:\"username\" required:\"true\"`\n\tPassword         string `json:\"password\" required:\"true\"`\n\tPlatform         string `json:\"platform\" required:\"true\" default:\"web\" type:\"select\" options:\"android,web,pc\"`\n\tRefreshToken     string `json:\"refresh_token\" required:\"true\" default:\"\"`\n\tCaptchaToken     string `json:\"captcha_token\" default:\"\"`\n\tDeviceID         string `json:\"device_id\"  required:\"false\" default:\"\"`\n\tDisableMediaLink bool   `json:\"disable_media_link\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:      \"PikPak\",\n\tLocalSort: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &PikPak{}\n\t})\n}\n"
  },
  {
    "path": "drivers/pikpak/types.go",
    "content": "package pikpak\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n)\n\ntype Files struct {\n\tFiles         []File `json:\"files\"`\n\tNextPageToken string `json:\"next_page_token\"`\n}\n\ntype File struct {\n\tId             string    `json:\"id\"`\n\tKind           string    `json:\"kind\"`\n\tName           string    `json:\"name\"`\n\tCreatedTime    time.Time `json:\"created_time\"`\n\tModifiedTime   time.Time `json:\"modified_time\"`\n\tHash           string    `json:\"hash\"`\n\tSize           string    `json:\"size\"`\n\tThumbnailLink  string    `json:\"thumbnail_link\"`\n\tWebContentLink string    `json:\"web_content_link\"`\n\tMedias         []Media   `json:\"medias\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\tsize, _ := strconv.ParseInt(f.Size, 10, 64)\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Id,\n\t\t\tName:     f.Name,\n\t\t\tSize:     size,\n\t\t\tCtime:    f.CreatedTime,\n\t\t\tModified: f.ModifiedTime,\n\t\t\tIsFolder: f.Kind == \"drive#folder\",\n\t\t\tHashInfo: utils.NewHashInfo(hash_extend.GCID, f.Hash),\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: f.ThumbnailLink,\n\t\t},\n\t}\n}\n\ntype Media struct {\n\tMediaId   string `json:\"media_id\"`\n\tMediaName string `json:\"media_name\"`\n\tVideo     struct {\n\t\tHeight     int    `json:\"height\"`\n\t\tWidth      int    `json:\"width\"`\n\t\tDuration   int    `json:\"duration\"`\n\t\tBitRate    int    `json:\"bit_rate\"`\n\t\tFrameRate  int    `json:\"frame_rate\"`\n\t\tVideoCodec string `json:\"video_codec\"`\n\t\tAudioCodec string `json:\"audio_codec\"`\n\t\tVideoType  string `json:\"video_type\"`\n\t} `json:\"video\"`\n\tLink struct {\n\t\tUrl    string    `json:\"url\"`\n\t\tToken  string    `json:\"token\"`\n\t\tExpire time.Time `json:\"expire\"`\n\t} `json:\"link\"`\n\tNeedMoreQuota  bool          `json:\"need_more_quota\"`\n\tVipTypes       []interface{} `json:\"vip_types\"`\n\tRedirectLink   string        `json:\"redirect_link\"`\n\tIconLink       string        `json:\"icon_link\"`\n\tIsDefault      bool          `json:\"is_default\"`\n\tPriority       int           `json:\"priority\"`\n\tIsOrigin       bool          `json:\"is_origin\"`\n\tResolutionName string        `json:\"resolution_name\"`\n\tIsVisible      bool          `json:\"is_visible\"`\n\tCategory       string        `json:\"category\"`\n}\n\ntype UploadTaskData struct {\n\tUploadType string `json:\"upload_type\"`\n\t// UPLOAD_TYPE_RESUMABLE\n\tResumable *struct {\n\t\tKind     string   `json:\"kind\"`\n\t\tParams   S3Params `json:\"params\"`\n\t\tProvider string   `json:\"provider\"`\n\t} `json:\"resumable\"`\n\n\tFile File `json:\"file\"`\n}\n\ntype S3Params struct {\n\tAccessKeyID     string    `json:\"access_key_id\"`\n\tAccessKeySecret string    `json:\"access_key_secret\"`\n\tBucket          string    `json:\"bucket\"`\n\tEndpoint        string    `json:\"endpoint\"`\n\tExpiration      time.Time `json:\"expiration\"`\n\tKey             string    `json:\"key\"`\n\tSecurityToken   string    `json:\"security_token\"`\n}\n\n// 添加离线下载响应\ntype OfflineDownloadResp struct {\n\tFile       *string     `json:\"file\"`\n\tTask       OfflineTask `json:\"task\"`\n\tUploadType string      `json:\"upload_type\"`\n\tURL        struct {\n\t\tKind string `json:\"kind\"`\n\t} `json:\"url\"`\n}\n\n// 离线下载列表\ntype OfflineListResp struct {\n\tExpiresIn     int64         `json:\"expires_in\"`\n\tNextPageToken string        `json:\"next_page_token\"`\n\tTasks         []OfflineTask `json:\"tasks\"`\n}\n\n// offlineTask\ntype OfflineTask struct {\n\tCallback          string            `json:\"callback\"`\n\tCreatedTime       string            `json:\"created_time\"`\n\tFileID            string            `json:\"file_id\"`\n\tFileName          string            `json:\"file_name\"`\n\tFileSize          string            `json:\"file_size\"`\n\tIconLink          string            `json:\"icon_link\"`\n\tID                string            `json:\"id\"`\n\tKind              string            `json:\"kind\"`\n\tMessage           string            `json:\"message\"`\n\tName              string            `json:\"name\"`\n\tParams            Params            `json:\"params\"`\n\tPhase             string            `json:\"phase\"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING\n\tProgress          int64             `json:\"progress\"`\n\tReferenceResource ReferenceResource `json:\"reference_resource\"`\n\tSpace             string            `json:\"space\"`\n\tStatusSize        int64             `json:\"status_size\"`\n\tStatuses          []string          `json:\"statuses\"`\n\tThirdTaskID       string            `json:\"third_task_id\"`\n\tType              string            `json:\"type\"`\n\tUpdatedTime       string            `json:\"updated_time\"`\n\tUserID            string            `json:\"user_id\"`\n}\n\ntype Params struct {\n\tAge         string  `json:\"age\"`\n\tMIMEType    *string `json:\"mime_type,omitempty\"`\n\tPredictType string  `json:\"predict_type\"`\n\tURL         string  `json:\"url\"`\n}\n\ntype ReferenceResource struct {\n\tType          string                 `json:\"@type\"`\n\tAudit         interface{}            `json:\"audit\"`\n\tHash          string                 `json:\"hash\"`\n\tIconLink      string                 `json:\"icon_link\"`\n\tID            string                 `json:\"id\"`\n\tKind          string                 `json:\"kind\"`\n\tMedias        []Media                `json:\"medias\"`\n\tMIMEType      string                 `json:\"mime_type\"`\n\tName          string                 `json:\"name\"`\n\tParams        map[string]interface{} `json:\"params\"`\n\tParentID      string                 `json:\"parent_id\"`\n\tPhase         string                 `json:\"phase\"`\n\tSize          string                 `json:\"size\"`\n\tSpace         string                 `json:\"space\"`\n\tStarred       bool                   `json:\"starred\"`\n\tTags          []string               `json:\"tags\"`\n\tThumbnailLink string                 `json:\"thumbnail_link\"`\n}\n\ntype ErrResp struct {\n\tErrorCode        int64  `json:\"error_code\"`\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ErrorDescription != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ErrorDescription: %s \", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)\n}\n\ntype CaptchaTokenRequest struct {\n\tAction       string            `json:\"action\"`\n\tCaptchaToken string            `json:\"captcha_token\"`\n\tClientID     string            `json:\"client_id\"`\n\tDeviceID     string            `json:\"device_id\"`\n\tMeta         map[string]string `json:\"meta\"`\n\tRedirectUri  string            `json:\"redirect_uri\"`\n}\n\ntype CaptchaTokenResponse struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tUrl          string `json:\"url\"`\n}\n\ntype AboutResponse struct {\n\tQuota struct {\n\t\tLimit         string `json:\"limit\"`\n\t\tUsage         string `json:\"usage\"`\n\t\tUsageInTrash  string `json:\"usage_in_trash\"`\n\t\tIsUnlimited   bool   `json:\"is_unlimited\"`\n\t\tComplimentary string `json:\"complimentary\"`\n\t} `json:\"quota\"`\n\tExpiresAt string `json:\"expires_at\"`\n\tUserType  int    `json:\"user_type\"`\n}\n"
  },
  {
    "path": "drivers/pikpak/util.go",
    "content": "package pikpak\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tnetutil \"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\t\"github.com/go-resty/resty/v2\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/pkg/errors\"\n)\n\nvar AndroidAlgorithms = []string{\n\t\"SOP04dGzk0TNO7t7t9ekDbAmx+eq0OI1ovEx\",\n\t\"nVBjhYiND4hZ2NCGyV5beamIr7k6ifAsAbl\",\n\t\"Ddjpt5B/Cit6EDq2a6cXgxY9lkEIOw4yC1GDF28KrA\",\n\t\"VVCogcmSNIVvgV6U+AochorydiSymi68YVNGiz\",\n\t\"u5ujk5sM62gpJOsB/1Gu/zsfgfZO\",\n\t\"dXYIiBOAHZgzSruaQ2Nhrqc2im\",\n\t\"z5jUTBSIpBN9g4qSJGlidNAutX6\",\n\t\"KJE2oveZ34du/g1tiimm\",\n}\n\nvar WebAlgorithms = []string{\n\t\"C9qPpZLN8ucRTaTiUMWYS9cQvWOE\",\n\t\"+r6CQVxjzJV6LCV\",\n\t\"F\",\n\t\"pFJRC\",\n\t\"9WXYIDGrwTCz2OiVlgZa90qpECPD6olt\",\n\t\"/750aCr4lm/Sly/c\",\n\t\"RB+DT/gZCrbV\",\n\t\"\",\n\t\"CyLsf7hdkIRxRm215hl\",\n\t\"7xHvLi2tOYP0Y92b\",\n\t\"ZGTXXxu8E/MIWaEDB+Sm/\",\n\t\"1UI3\",\n\t\"E7fP5Pfijd+7K+t6Tg/NhuLq0eEUVChpJSkrKxpO\",\n\t\"ihtqpG6FMt65+Xk+tWUH2\",\n\t\"NhXXU9rg4XXdzo7u5o\",\n}\n\nvar PCAlgorithms = []string{\n\t\"KHBJ07an7ROXDoK7Db\",\n\t\"G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE\",\n\t\"JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb\",\n\t\"fQnw/AmSlbbI91Ik15gpddGgyU7U\",\n\t\"/Dv9JdPYSj3sHiWjouR95NTQff\",\n\t\"yGx2zuTjbWENZqecNI+edrQgqmZKP\",\n\t\"ljrbSzdHLwbqcRn\",\n\t\"lSHAsqCkGDGxQqqwrVu\",\n\t\"TsWXI81fD1\",\n\t\"vk7hBjawK/rOSrSWajtbMk95nfgf3\",\n}\n\nconst (\n\tOSSUserAgent               = \"aliyun-sdk-android/2.9.13(Linux/Android 14/M2004j7ac;UKQ1.231108.001)\"\n\tOssSecurityTokenHeaderName = \"X-OSS-Security-Token\"\n\tThreadsNum                 = 10\n)\n\nconst (\n\tAndroidClientID      = \"YNxT9w7GMdWvEOKa\"\n\tAndroidClientSecret  = \"dbw2OtmVEeuUvIptb1Coyg\"\n\tAndroidClientVersion = \"1.53.2\"\n\tAndroidPackageName   = \"com.pikcloud.pikpak\"\n\tAndroidSdkVersion    = \"2.0.6.206003\"\n\tWebClientID          = \"YUMx5nI8ZU8Ap8pm\"\n\tWebClientSecret      = \"dbw2OtmVEeuUvIptb1Coyg\"\n\tWebClientVersion     = \"2.0.0\"\n\tWebPackageName       = \"mypikpak.com\"\n\tWebSdkVersion        = \"8.0.3\"\n\tPCClientID           = \"YvtoWO6GNHiuCl7x\"\n\tPCClientSecret       = \"1NIH5R1IEe2pAxZE3hv3uA\"\n\tPCClientVersion      = \"undefined\" // 2.6.11.4955\n\tPCPackageName        = \"mypikpak.com\"\n\tPCSdkVersion         = \"8.0.3\"\n)\n\nfunc (d *PikPak) login() error {\n\t// 检查用户名和密码是否为空\n\tif d.Addition.Username == \"\" || d.Addition.Password == \"\" {\n\t\treturn errors.New(\"username or password is empty\")\n\t}\n\n\turl := \"https://user.mypikpak.net/v1/auth/signin\"\n\t// 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)\n\tif d.GetCaptchaToken() == \"\" {\n\t\tif err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar e ErrResp\n\tres, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).SetBody(base.Json{\n\t\t\"captcha_token\": d.GetCaptchaToken(),\n\t\t\"client_id\":     d.ClientID,\n\t\t\"client_secret\": d.ClientSecret,\n\t\t\"username\":      d.Username,\n\t\t\"password\":      d.Password,\n\t}).SetQueryParam(\"client_id\", d.ClientID).Post(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.ErrorCode != 0 {\n\t\treturn &e\n\t}\n\tdata := res.Body()\n\td.RefreshToken = jsoniter.Get(data, \"refresh_token\").ToString()\n\td.AccessToken = jsoniter.Get(data, \"access_token\").ToString()\n\td.Common.SetUserID(jsoniter.Get(data, \"sub\").ToString())\n\treturn nil\n}\n\nfunc (d *PikPak) refreshToken(refreshToken string) error {\n\turl := \"https://user.mypikpak.net/v1/auth/token\"\n\tvar e ErrResp\n\tres, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).\n\t\tSetHeader(\"user-agent\", \"\").SetBody(base.Json{\n\t\t\"client_id\":     d.ClientID,\n\t\t\"client_secret\": d.ClientSecret,\n\t\t\"grant_type\":    \"refresh_token\",\n\t\t\"refresh_token\": refreshToken,\n\t}).SetQueryParam(\"client_id\", d.ClientID).Post(url)\n\tif err != nil {\n\t\td.Status = err.Error()\n\t\top.MustSaveDriverStorage(d)\n\t\treturn err\n\t}\n\tif e.ErrorCode != 0 {\n\t\tif e.ErrorCode == 4126 {\n\t\t\t// 1. 未填写 username 或 password\n\t\t\tif d.Addition.Username == \"\" || d.Addition.Password == \"\" {\n\t\t\t\treturn errors.New(\"refresh_token invalid, please re-provide refresh_token\")\n\t\t\t} else {\n\t\t\t\t// refresh_token invalid, re-login\n\t\t\t\treturn d.login()\n\t\t\t}\n\t\t}\n\t\td.Status = e.Error()\n\t\top.MustSaveDriverStorage(d)\n\t\treturn errors.New(e.Error())\n\t}\n\tdata := res.Body()\n\td.Status = \"work\"\n\td.RefreshToken = jsoniter.Get(data, \"refresh_token\").ToString()\n\td.AccessToken = jsoniter.Get(data, \"access_token\").ToString()\n\td.Common.SetUserID(jsoniter.Get(data, \"sub\").ToString())\n\td.Addition.RefreshToken = d.RefreshToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t//\"Authorization\":   \"Bearer \" + d.AccessToken,\n\t\t\"User-Agent\":      d.GetUserAgent(),\n\t\t\"X-Device-ID\":     d.GetDeviceID(),\n\t\t\"X-Captcha-Token\": d.GetCaptchaToken(),\n\t})\n\tif d.AccessToken != \"\" {\n\t\treq.SetHeader(\"Authorization\", \"Bearer \"+d.AccessToken)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch e.ErrorCode {\n\tcase 0:\n\t\treturn res.Body(), nil\n\tcase 4122, 4121, 16:\n\t\t// access_token 过期\n\t\tif err1 := d.refreshToken(d.RefreshToken); err1 != nil {\n\t\t\treturn nil, err1\n\t\t}\n\t\treturn d.request(url, method, callback, resp)\n\tcase 9: // 验证码token过期\n\t\tif err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn d.request(url, method, callback, resp)\n\tcase 10: // 操作频繁\n\t\treturn nil, errors.New(e.ErrorDescription)\n\tdefault:\n\t\treturn nil, errors.New(e.Error())\n\t}\n}\n\nfunc (d *PikPak) getFiles(id string) ([]File, error) {\n\tres := make([]File, 0)\n\tpageToken := \"first\"\n\tfor pageToken != \"\" {\n\t\tif pageToken == \"first\" {\n\t\t\tpageToken = \"\"\n\t\t}\n\t\tquery := map[string]string{\n\t\t\t\"parent_id\":      id,\n\t\t\t\"thumbnail_size\": \"SIZE_LARGE\",\n\t\t\t\"with_audit\":     \"true\",\n\t\t\t\"limit\":          \"100\",\n\t\t\t\"filters\":        `{\"phase\":{\"eq\":\"PHASE_TYPE_COMPLETE\"},\"trashed\":{\"eq\":false}}`,\n\t\t\t\"page_token\":     pageToken,\n\t\t}\n\t\tvar resp Files\n\t\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/files\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpageToken = resp.NextPageToken\n\t\tres = append(res, resp.Files...)\n\t}\n\treturn res, nil\n}\n\nfunc GetAction(method string, url string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(url)[1]\n\treturn method + \":\" + urlpath\n}\n\ntype Common struct {\n\tclient       *resty.Client\n\tCaptchaToken string\n\tUserID       string\n\t// 必要值,签名相关\n\tClientID      string\n\tClientSecret  string\n\tClientVersion string\n\tPackageName   string\n\tAlgorithms    []string\n\tDeviceID      string\n\tUserAgent     string\n\t// 验证码token刷新成功回调\n\tRefreshCTokenCk func(token string)\n}\n\nfunc generateDeviceSign(deviceID, packageName string) string {\n\n\tsignatureBase := fmt.Sprintf(\"%s%s%s%s\", deviceID, packageName, \"1\", \"appkey\")\n\n\tsha1Hash := sha1.New()\n\tsha1Hash.Write([]byte(signatureBase))\n\tsha1Result := sha1Hash.Sum(nil)\n\n\tsha1String := hex.EncodeToString(sha1Result)\n\n\tmd5Hash := md5.New()\n\tmd5Hash.Write([]byte(sha1String))\n\tmd5Result := md5Hash.Sum(nil)\n\n\tmd5String := hex.EncodeToString(md5Result)\n\n\tdeviceSign := fmt.Sprintf(\"div101.%s%s\", deviceID, md5String)\n\n\treturn deviceSign\n}\n\nfunc BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {\n\tdeviceSign := generateDeviceSign(deviceID, packageName)\n\tvar sb strings.Builder\n\n\tsb.WriteString(fmt.Sprintf(\"ANDROID-%s/%s \", appName, clientVersion))\n\tsb.WriteString(\"protocolVersion/200 \")\n\tsb.WriteString(\"accesstype/ \")\n\tsb.WriteString(fmt.Sprintf(\"clientid/%s \", clientID))\n\tsb.WriteString(fmt.Sprintf(\"clientversion/%s \", clientVersion))\n\tsb.WriteString(\"action_type/ \")\n\tsb.WriteString(\"networktype/WIFI \")\n\tsb.WriteString(\"sessionid/ \")\n\tsb.WriteString(fmt.Sprintf(\"deviceid/%s \", deviceID))\n\tsb.WriteString(\"providername/NONE \")\n\tsb.WriteString(fmt.Sprintf(\"devicesign/%s \", deviceSign))\n\tsb.WriteString(\"refresh_token/ \")\n\tsb.WriteString(fmt.Sprintf(\"sdkversion/%s \", sdkVersion))\n\tsb.WriteString(fmt.Sprintf(\"datetime/%d \", time.Now().UnixMilli()))\n\tsb.WriteString(fmt.Sprintf(\"usrno/%s \", userID))\n\tsb.WriteString(fmt.Sprintf(\"appname/android-%s \", appName))\n\tsb.WriteString(fmt.Sprintf(\"session_origin/ \"))\n\tsb.WriteString(fmt.Sprintf(\"grant_type/ \"))\n\tsb.WriteString(fmt.Sprintf(\"appid/ \"))\n\tsb.WriteString(fmt.Sprintf(\"clientip/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicename/Xiaomi_M2004j7ac \"))\n\tsb.WriteString(fmt.Sprintf(\"osversion/13 \"))\n\tsb.WriteString(fmt.Sprintf(\"platformversion/10 \"))\n\tsb.WriteString(fmt.Sprintf(\"accessmode/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicemodel/M2004J7AC \"))\n\n\treturn sb.String()\n}\n\nfunc (c *Common) SetDeviceID(deviceID string) {\n\tc.DeviceID = deviceID\n}\n\nfunc (c *Common) SetUserID(userID string) {\n\tc.UserID = userID\n}\n\nfunc (c *Common) SetUserAgent(userAgent string) {\n\tc.UserAgent = userAgent\n}\n\nfunc (c *Common) SetCaptchaToken(captchaToken string) {\n\tc.CaptchaToken = captchaToken\n}\nfunc (c *Common) GetCaptchaToken() string {\n\treturn c.CaptchaToken\n}\n\nfunc (c *Common) GetUserAgent() string {\n\treturn c.UserAgent\n}\n\nfunc (c *Common) GetDeviceID() string {\n\treturn c.DeviceID\n}\n\nfunc (c *Common) GetUserID() string {\n\treturn c.UserID\n}\n\n// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)\nfunc (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {\n\tmetas := map[string]string{\n\t\t\"client_version\": d.ClientVersion,\n\t\t\"package_name\":   d.PackageName,\n\t\t\"user_id\":        userID,\n\t}\n\tmetas[\"timestamp\"], metas[\"captcha_sign\"] = d.Common.GetCaptchaSign()\n\treturn d.refreshCaptchaToken(action, metas)\n}\n\n// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)\nfunc (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {\n\tmetas := make(map[string]string)\n\tif ok, _ := regexp.MatchString(`\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*`, username); ok {\n\t\tmetas[\"email\"] = username\n\t} else if len(username) >= 11 && len(username) <= 18 {\n\t\tmetas[\"phone_number\"] = username\n\t} else {\n\t\tmetas[\"username\"] = username\n\t}\n\treturn d.refreshCaptchaToken(action, metas)\n}\n\n// GetCaptchaSign 获取验证码签名\nfunc (c *Common) GetCaptchaSign() (timestamp, sign string) {\n\ttimestamp = fmt.Sprint(time.Now().UnixMilli())\n\tstr := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)\n\tfor _, algorithm := range c.Algorithms {\n\t\tstr = utils.GetMD5EncodeStr(str + algorithm)\n\t}\n\tsign = \"1.\" + str\n\treturn\n}\n\n// refreshCaptchaToken 刷新CaptchaToken\nfunc (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {\n\tparam := CaptchaTokenRequest{\n\t\tAction:       action,\n\t\tCaptchaToken: d.GetCaptchaToken(),\n\t\tClientID:     d.ClientID,\n\t\tDeviceID:     d.GetDeviceID(),\n\t\tMeta:         metas,\n\t\tRedirectUri:  \"xlaccsdk01://xbase.cloud/callback?state=harbor\",\n\t}\n\tvar e ErrResp\n\tvar resp CaptchaTokenResponse\n\t_, err := d.request(\"https://user.mypikpak.net/v1/shield/captcha/init\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetError(&e).SetBody(param).SetQueryParam(\"client_id\", d.ClientID)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif e.IsError() {\n\t\treturn errors.New(e.Error())\n\t}\n\n\tif resp.Url != \"\" {\n\t\treturn fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, resp.Url)\n\t}\n\n\tif d.Common.RefreshCTokenCk != nil {\n\t\td.Common.RefreshCTokenCk(resp.CaptchaToken)\n\t}\n\td.Common.SetCaptchaToken(resp.CaptchaToken)\n\treturn nil\n}\n\nfunc (d *PikPak) UploadByOSS(ctx context.Context, params *S3Params, s model.FileStreamer, up driver.UpdateProgress) error {\n\tossClient, err := netutil.NewOSSClient(params.Endpoint, params.AccessKeyID, params.AccessKeySecret)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbucket, err := ossClient.Bucket(params.Bucket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = bucket.PutObject(params.Key, driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t}), OssOption(params)...)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *PikPak) UploadByMultipart(ctx context.Context, params *S3Params, fileSize int64, s model.FileStreamer, up driver.UpdateProgress) error {\n\ttmpF, err := s.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tchunks    []oss.FileChunk\n\t\tparts     []oss.UploadPart\n\t\timur      oss.InitiateMultipartUploadResult\n\t\tossClient *oss.Client\n\t\tbucket    *oss.Bucket\n\t)\n\n\tif ossClient, err = netutil.NewOSSClient(params.Endpoint, params.AccessKeyID, params.AccessKeySecret); err != nil {\n\t\treturn err\n\t}\n\n\tif bucket, err = ossClient.Bucket(params.Bucket); err != nil {\n\t\treturn err\n\t}\n\n\tticker := time.NewTicker(time.Hour * 12)\n\tdefer ticker.Stop()\n\t// 设置超时\n\ttimeout := time.NewTimer(time.Hour * 24)\n\n\tif chunks, err = SplitFile(fileSize); err != nil {\n\t\treturn err\n\t}\n\n\tif imur, err = bucket.InitiateMultipartUpload(params.Key,\n\t\toss.SetHeader(OssSecurityTokenHeaderName, params.SecurityToken),\n\t\toss.UserAgentHeader(OSSUserAgent),\n\t); err != nil {\n\t\treturn err\n\t}\n\n\twg := sync.WaitGroup{}\n\twg.Add(len(chunks))\n\n\tchunksCh := make(chan oss.FileChunk)\n\terrCh := make(chan error)\n\tUploadedPartsCh := make(chan oss.UploadPart)\n\tquit := make(chan struct{})\n\n\t// producer\n\tgo chunksProducer(chunksCh, chunks)\n\tgo func() {\n\t\twg.Wait()\n\t\tquit <- struct{}{}\n\t}()\n\n\tcompletedNum := atomic.Int32{}\n\t// consumers\n\tfor i := 0; i < ThreadsNum; i++ {\n\t\tgo func(threadId int) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"recovered in %v\", r)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tfor chunk := range chunksCh {\n\t\t\t\tvar part oss.UploadPart // 出现错误就继续尝试，共尝试3次\n\t\t\t\tfor retry := 0; retry < 3; retry++ {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase <-ticker.C:\n\t\t\t\t\t\terrCh <- errors.Wrap(err, \"ossToken 过期\")\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf := make([]byte, chunk.Size)\n\t\t\t\t\tif _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tb := driver.NewLimitedUploadStream(ctx, bytes.NewReader(buf))\n\t\t\t\t\tif part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, OssOption(params)...); err == nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- errors.Wrap(err, fmt.Sprintf(\"上传 %s 的第%d个分片时出现错误：%v\", s.GetName(), chunk.Number, err))\n\t\t\t\t} else {\n\t\t\t\t\tnum := completedNum.Add(1)\n\t\t\t\t\tup(float64(num) * 100.0 / float64(len(chunks)))\n\t\t\t\t}\n\t\t\t\tUploadedPartsCh <- part\n\t\t\t}\n\t\t}(i)\n\t}\n\n\tgo func() {\n\t\tfor part := range UploadedPartsCh {\n\t\t\tparts = append(parts, part)\n\t\t\twg.Done()\n\t\t}\n\t}()\nLOOP:\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\t// ossToken 过期\n\t\t\treturn err\n\t\tcase <-quit:\n\t\t\tbreak LOOP\n\t\tcase <-errCh:\n\t\t\treturn err\n\t\tcase <-timeout.C:\n\t\t\treturn fmt.Errorf(\"time out\")\n\t\t}\n\t}\n\n\t// EOF错误是xml的Unmarshal导致的，响应其实是json格式，所以实际上上传是成功的\n\tif _, err = bucket.CompleteMultipartUpload(imur, parts, OssOption(params)...); err != nil && !errors.Is(err, io.EOF) {\n\t\t// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误，实际上上传是成功的\n\t\tif filename := filepath.Base(s.GetName()); !strings.ContainsAny(filename, \"&<\") {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {\n\tfor _, chunk := range chunks {\n\t\tch <- chunk\n\t}\n}\n\nfunc SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {\n\tfor i := int64(1); i < 10; i++ {\n\t\tif fileSize < i*utils.GB { // 文件大小小于iGB时分为i*100片\n\t\t\tif chunks, err = SplitFileByPartNum(fileSize, int(i*100)); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\tif fileSize > 9*utils.GB { // 文件大小大于9GB时分为1000片\n\t\tif chunks, err = SplitFileByPartNum(fileSize, 1000); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\t// 单个分片大小不能小于1MB\n\tif chunks[0].Size < 1*utils.MB {\n\t\tif chunks, err = SplitFileByPartSize(fileSize, 1*utils.MB); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// SplitFileByPartNum splits big file into parts by the num of parts.\n// Split the file with specified parts count, returns the split result when error is nil.\nfunc SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {\n\tif chunkNum <= 0 || chunkNum > 10000 {\n\t\treturn nil, errors.New(\"chunkNum invalid\")\n\t}\n\n\tif int64(chunkNum) > fileSize {\n\t\treturn nil, errors.New(\"oss: chunkNum invalid\")\n\t}\n\n\tvar chunks []oss.FileChunk\n\tchunk := oss.FileChunk{}\n\tchunkN := (int64)(chunkNum)\n\tfor i := int64(0); i < chunkN; i++ {\n\t\tchunk.Number = int(i + 1)\n\t\tchunk.Offset = i * (fileSize / chunkN)\n\t\tif i == chunkN-1 {\n\t\t\tchunk.Size = fileSize/chunkN + fileSize%chunkN\n\t\t} else {\n\t\t\tchunk.Size = fileSize / chunkN\n\t\t}\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\treturn chunks, nil\n}\n\n// SplitFileByPartSize splits big file into parts by the size of parts.\n// Splits the file by the part size. Returns the FileChunk when error is nil.\nfunc SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {\n\tif chunkSize <= 0 {\n\t\treturn nil, errors.New(\"chunkSize invalid\")\n\t}\n\n\tchunkN := fileSize / chunkSize\n\tif chunkN >= 10000 {\n\t\treturn nil, errors.New(\"Too many parts, please increase part size\")\n\t}\n\n\tvar chunks []oss.FileChunk\n\tchunk := oss.FileChunk{}\n\tfor i := int64(0); i < chunkN; i++ {\n\t\tchunk.Number = int(i + 1)\n\t\tchunk.Offset = i * chunkSize\n\t\tchunk.Size = chunkSize\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\tif fileSize%chunkSize > 0 {\n\t\tchunk.Number = len(chunks) + 1\n\t\tchunk.Offset = int64(len(chunks)) * chunkSize\n\t\tchunk.Size = fileSize % chunkSize\n\t\tchunks = append(chunks, chunk)\n\t}\n\n\treturn chunks, nil\n}\n\n// OssOption get options\nfunc OssOption(params *S3Params) []oss.Option {\n\toptions := []oss.Option{\n\t\toss.SetHeader(OssSecurityTokenHeaderName, params.SecurityToken),\n\t\toss.UserAgentHeader(OSSUserAgent),\n\t}\n\treturn options\n}\n"
  },
  {
    "path": "drivers/pikpak_share/driver.go",
    "content": "package pikpak_share\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype PikPakShare struct {\n\tmodel.Storage\n\tAddition\n\t*Common\n\tPassCodeToken string\n}\n\nfunc (d *PikPakShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *PikPakShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *PikPakShare) Init(ctx context.Context) error {\n\tif d.Common == nil {\n\t\td.Common = &Common{\n\t\t\tDeviceID:  utils.GetMD5EncodeStr(d.Addition.ShareId + d.Addition.SharePwd + time.Now().String()),\n\t\t\tUserAgent: \"\",\n\t\t\tRefreshCTokenCk: func(token string) {\n\t\t\t\td.Common.CaptchaToken = token\n\t\t\t\top.MustSaveDriverStorage(d)\n\t\t\t},\n\t\t}\n\t}\n\n\tif d.Addition.DeviceID != \"\" {\n\t\td.SetDeviceID(d.Addition.DeviceID)\n\t} else {\n\t\td.Addition.DeviceID = d.Common.DeviceID\n\t\top.MustSaveDriverStorage(d)\n\t}\n\n\tif d.Platform == \"android\" {\n\t\td.ClientID = AndroidClientID\n\t\td.ClientSecret = AndroidClientSecret\n\t\td.ClientVersion = AndroidClientVersion\n\t\td.PackageName = AndroidPackageName\n\t\td.Algorithms = AndroidAlgorithms\n\t\td.UserAgent = BuildCustomUserAgent(d.GetDeviceID(), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, \"\")\n\t} else if d.Platform == \"web\" {\n\t\td.ClientID = WebClientID\n\t\td.ClientSecret = WebClientSecret\n\t\td.ClientVersion = WebClientVersion\n\t\td.PackageName = WebPackageName\n\t\td.Algorithms = WebAlgorithms\n\t\td.UserAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n\t} else if d.Platform == \"pc\" {\n\t\td.ClientID = PCClientID\n\t\td.ClientSecret = PCClientSecret\n\t\td.ClientVersion = PCClientVersion\n\t\td.PackageName = PCPackageName\n\t\td.Algorithms = PCAlgorithms\n\t\td.UserAgent = \"MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.6.11.4955 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36\"\n\t}\n\n\t// 获取CaptchaToken\n\terr := d.RefreshCaptchaToken(GetAction(http.MethodGet, \"https://api-drive.mypikpak.net/drive/v1/share:batch_file_info\"), \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif d.SharePwd != \"\" {\n\t\treturn d.getSharePassToken()\n\t}\n\n\treturn nil\n}\n\nfunc (d *PikPakShare) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *PikPakShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp ShareResp\n\tquery := map[string]string{\n\t\t\"share_id\":        d.ShareId,\n\t\t\"file_id\":         file.GetID(),\n\t\t\"pass_code_token\": d.PassCodeToken,\n\t}\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/share/file_info\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdownloadUrl := resp.FileInfo.WebContentLink\n\tif downloadUrl == \"\" && len(resp.FileInfo.Medias) > 0 {\n\t\t// 使用转码后的链接\n\t\tif d.Addition.UseTransCodingAddress && len(resp.FileInfo.Medias) > 1 {\n\t\t\tdownloadUrl = resp.FileInfo.Medias[1].Link.Url\n\t\t} else {\n\t\t\tdownloadUrl = resp.FileInfo.Medias[0].Link.Url\n\t\t}\n\n\t}\n\n\treturn &model.Link{\n\t\tURL: downloadUrl,\n\t}, nil\n}\n\nvar _ driver.Driver = (*PikPakShare)(nil)\n"
  },
  {
    "path": "drivers/pikpak_share/meta.go",
    "content": "package pikpak_share\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tShareId               string `json:\"share_id\" required:\"true\"`\n\tSharePwd              string `json:\"share_pwd\"`\n\tPlatform              string `json:\"platform\" default:\"web\" required:\"true\" type:\"select\" options:\"android,web,pc\"`\n\tDeviceID              string `json:\"device_id\"  required:\"false\" default:\"\"`\n\tUseTransCodingAddress bool   `json:\"use_transcoding_address\" required:\"true\" default:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:      \"PikPakShare\",\n\tLocalSort: true,\n\tNoUpload:  true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &PikPakShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/pikpak_share/types.go",
    "content": "package pikpak_share\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype ShareResp struct {\n\tShareStatus     string `json:\"share_status\"`\n\tShareStatusText string `json:\"share_status_text\"`\n\tFileInfo        File   `json:\"file_info\"`\n\tFiles           []File `json:\"files\"`\n\tNextPageToken   string `json:\"next_page_token\"`\n\tPassCodeToken   string `json:\"pass_code_token\"`\n}\n\ntype File struct {\n\tId             string    `json:\"id\"`\n\tShareId        string    `json:\"share_id\"`\n\tKind           string    `json:\"kind\"`\n\tName           string    `json:\"name\"`\n\tModifiedTime   time.Time `json:\"modified_time\"`\n\tSize           string    `json:\"size\"`\n\tThumbnailLink  string    `json:\"thumbnail_link\"`\n\tWebContentLink string    `json:\"web_content_link\"`\n\tMedias         []Media   `json:\"medias\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\tsize, _ := strconv.ParseInt(f.Size, 10, 64)\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Id,\n\t\t\tName:     f.Name,\n\t\t\tSize:     size,\n\t\t\tModified: f.ModifiedTime,\n\t\t\tIsFolder: f.Kind == \"drive#folder\",\n\t\t},\n\t\tThumbnail: model.Thumbnail{\n\t\t\tThumbnail: f.ThumbnailLink,\n\t\t},\n\t}\n}\n\ntype Media struct {\n\tMediaId   string `json:\"media_id\"`\n\tMediaName string `json:\"media_name\"`\n\tVideo     struct {\n\t\tHeight     int    `json:\"height\"`\n\t\tWidth      int    `json:\"width\"`\n\t\tDuration   int    `json:\"duration\"`\n\t\tBitRate    int    `json:\"bit_rate\"`\n\t\tFrameRate  int    `json:\"frame_rate\"`\n\t\tVideoCodec string `json:\"video_codec\"`\n\t\tAudioCodec string `json:\"audio_codec\"`\n\t\tVideoType  string `json:\"video_type\"`\n\t} `json:\"video\"`\n\tLink struct {\n\t\tUrl    string    `json:\"url\"`\n\t\tToken  string    `json:\"token\"`\n\t\tExpire time.Time `json:\"expire\"`\n\t} `json:\"link\"`\n\tNeedMoreQuota  bool          `json:\"need_more_quota\"`\n\tVipTypes       []interface{} `json:\"vip_types\"`\n\tRedirectLink   string        `json:\"redirect_link\"`\n\tIconLink       string        `json:\"icon_link\"`\n\tIsDefault      bool          `json:\"is_default\"`\n\tPriority       int           `json:\"priority\"`\n\tIsOrigin       bool          `json:\"is_origin\"`\n\tResolutionName string        `json:\"resolution_name\"`\n\tIsVisible      bool          `json:\"is_visible\"`\n\tCategory       string        `json:\"category\"`\n}\n\ntype CaptchaTokenRequest struct {\n\tAction       string            `json:\"action\"`\n\tCaptchaToken string            `json:\"captcha_token\"`\n\tClientID     string            `json:\"client_id\"`\n\tDeviceID     string            `json:\"device_id\"`\n\tMeta         map[string]string `json:\"meta\"`\n\tRedirectUri  string            `json:\"redirect_uri\"`\n}\n\ntype CaptchaTokenResponse struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tUrl          string `json:\"url\"`\n}\n\ntype ErrResp struct {\n\tErrorCode        int64  `json:\"error_code\"`\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ErrorDescription != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ErrorDescription: %s \", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)\n}\n"
  },
  {
    "path": "drivers/pikpak_share/util.go",
    "content": "package pikpak_share\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nvar AndroidAlgorithms = []string{\n\t\"SOP04dGzk0TNO7t7t9ekDbAmx+eq0OI1ovEx\",\n\t\"nVBjhYiND4hZ2NCGyV5beamIr7k6ifAsAbl\",\n\t\"Ddjpt5B/Cit6EDq2a6cXgxY9lkEIOw4yC1GDF28KrA\",\n\t\"VVCogcmSNIVvgV6U+AochorydiSymi68YVNGiz\",\n\t\"u5ujk5sM62gpJOsB/1Gu/zsfgfZO\",\n\t\"dXYIiBOAHZgzSruaQ2Nhrqc2im\",\n\t\"z5jUTBSIpBN9g4qSJGlidNAutX6\",\n\t\"KJE2oveZ34du/g1tiimm\",\n}\n\nvar WebAlgorithms = []string{\n\t\"C9qPpZLN8ucRTaTiUMWYS9cQvWOE\",\n\t\"+r6CQVxjzJV6LCV\",\n\t\"F\",\n\t\"pFJRC\",\n\t\"9WXYIDGrwTCz2OiVlgZa90qpECPD6olt\",\n\t\"/750aCr4lm/Sly/c\",\n\t\"RB+DT/gZCrbV\",\n\t\"\",\n\t\"CyLsf7hdkIRxRm215hl\",\n\t\"7xHvLi2tOYP0Y92b\",\n\t\"ZGTXXxu8E/MIWaEDB+Sm/\",\n\t\"1UI3\",\n\t\"E7fP5Pfijd+7K+t6Tg/NhuLq0eEUVChpJSkrKxpO\",\n\t\"ihtqpG6FMt65+Xk+tWUH2\",\n\t\"NhXXU9rg4XXdzo7u5o\",\n}\n\nvar PCAlgorithms = []string{\n\t\"KHBJ07an7ROXDoK7Db\",\n\t\"G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE\",\n\t\"JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb\",\n\t\"fQnw/AmSlbbI91Ik15gpddGgyU7U\",\n\t\"/Dv9JdPYSj3sHiWjouR95NTQff\",\n\t\"yGx2zuTjbWENZqecNI+edrQgqmZKP\",\n\t\"ljrbSzdHLwbqcRn\",\n\t\"lSHAsqCkGDGxQqqwrVu\",\n\t\"TsWXI81fD1\",\n\t\"vk7hBjawK/rOSrSWajtbMk95nfgf3\",\n}\n\nconst (\n\tAndroidClientID      = \"YNxT9w7GMdWvEOKa\"\n\tAndroidClientSecret  = \"dbw2OtmVEeuUvIptb1Coyg\"\n\tAndroidClientVersion = \"1.53.2\"\n\tAndroidPackageName   = \"com.pikcloud.pikpak\"\n\tAndroidSdkVersion    = \"2.0.6.206003\"\n\tWebClientID          = \"YUMx5nI8ZU8Ap8pm\"\n\tWebClientSecret      = \"dbw2OtmVEeuUvIptb1Coyg\"\n\tWebClientVersion     = \"2.0.0\"\n\tWebPackageName       = \"mypikpak.com\"\n\tWebSdkVersion        = \"8.0.3\"\n\tPCClientID           = \"YvtoWO6GNHiuCl7x\"\n\tPCClientSecret       = \"1NIH5R1IEe2pAxZE3hv3uA\"\n\tPCClientVersion      = \"undefined\" // 2.6.11.4955\n\tPCPackageName        = \"mypikpak.com\"\n\tPCSdkVersion         = \"8.0.3\"\n)\n\nfunc (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"User-Agent\":      d.GetUserAgent(),\n\t\t\"X-Client-ID\":     d.GetClientID(),\n\t\t\"X-Device-ID\":     d.GetDeviceID(),\n\t\t\"X-Captcha-Token\": d.GetCaptchaToken(),\n\t})\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch e.ErrorCode {\n\tcase 0:\n\t\treturn res.Body(), nil\n\tcase 9: // 验证码token过期\n\t\tif err = d.RefreshCaptchaToken(GetAction(method, url), \"\"); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn d.request(url, method, callback, resp)\n\tcase 10: // 操作频繁\n\t\treturn nil, errors.New(e.ErrorDescription)\n\tdefault:\n\t\treturn nil, errors.New(e.Error())\n\t}\n}\n\nfunc (d *PikPakShare) getSharePassToken() error {\n\tquery := map[string]string{\n\t\t\"share_id\":       d.ShareId,\n\t\t\"pass_code\":      d.SharePwd,\n\t\t\"thumbnail_size\": \"SIZE_LARGE\",\n\t\t\"limit\":          \"100\",\n\t}\n\tvar resp ShareResp\n\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/share\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.PassCodeToken = resp.PassCodeToken\n\treturn nil\n}\n\nfunc (d *PikPakShare) getFiles(id string) ([]File, error) {\n\tres := make([]File, 0)\n\tpageToken := \"first\"\n\tfor pageToken != \"\" {\n\t\tif pageToken == \"first\" {\n\t\t\tpageToken = \"\"\n\t\t}\n\t\tquery := map[string]string{\n\t\t\t\"parent_id\":       id,\n\t\t\t\"share_id\":        d.ShareId,\n\t\t\t\"thumbnail_size\":  \"SIZE_LARGE\",\n\t\t\t\"with_audit\":      \"true\",\n\t\t\t\"limit\":           \"100\",\n\t\t\t\"filters\":         `{\"phase\":{\"eq\":\"PHASE_TYPE_COMPLETE\"},\"trashed\":{\"eq\":false}}`,\n\t\t\t\"page_token\":      pageToken,\n\t\t\t\"pass_code_token\": d.PassCodeToken,\n\t\t}\n\t\tvar resp ShareResp\n\t\t_, err := d.request(\"https://api-drive.mypikpak.net/drive/v1/share/detail\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resp.ShareStatus != \"OK\" {\n\t\t\tif resp.ShareStatus == \"PASS_CODE_EMPTY\" || resp.ShareStatus == \"PASS_CODE_ERROR\" {\n\t\t\t\terr = d.getSharePassToken()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn d.getFiles(id)\n\t\t\t}\n\t\t\treturn nil, errors.New(resp.ShareStatusText)\n\t\t}\n\t\tpageToken = resp.NextPageToken\n\t\tres = append(res, resp.Files...)\n\t}\n\treturn res, nil\n}\n\nfunc GetAction(method string, url string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(url)[1]\n\treturn method + \":\" + urlpath\n}\n\ntype Common struct {\n\tclient       *resty.Client\n\tCaptchaToken string\n\t// 必要值,签名相关\n\tClientID      string\n\tClientSecret  string\n\tClientVersion string\n\tPackageName   string\n\tAlgorithms    []string\n\tDeviceID      string\n\tUserAgent     string\n\t// 验证码token刷新成功回调\n\tRefreshCTokenCk func(token string)\n}\n\nfunc (c *Common) SetUserAgent(userAgent string) {\n\tc.UserAgent = userAgent\n}\n\nfunc (c *Common) SetCaptchaToken(captchaToken string) {\n\tc.CaptchaToken = captchaToken\n}\n\nfunc (c *Common) SetDeviceID(deviceID string) {\n\tc.DeviceID = deviceID\n}\n\nfunc (c *Common) GetCaptchaToken() string {\n\treturn c.CaptchaToken\n}\n\nfunc (c *Common) GetClientID() string {\n\treturn c.ClientID\n}\n\nfunc (c *Common) GetUserAgent() string {\n\treturn c.UserAgent\n}\n\nfunc (c *Common) GetDeviceID() string {\n\treturn c.DeviceID\n}\n\nfunc generateDeviceSign(deviceID, packageName string) string {\n\n\tsignatureBase := fmt.Sprintf(\"%s%s%s%s\", deviceID, packageName, \"1\", \"appkey\")\n\n\tsha1Hash := sha1.New()\n\tsha1Hash.Write([]byte(signatureBase))\n\tsha1Result := sha1Hash.Sum(nil)\n\n\tsha1String := hex.EncodeToString(sha1Result)\n\n\tmd5Hash := md5.New()\n\tmd5Hash.Write([]byte(sha1String))\n\tmd5Result := md5Hash.Sum(nil)\n\n\tmd5String := hex.EncodeToString(md5Result)\n\n\tdeviceSign := fmt.Sprintf(\"div101.%s%s\", deviceID, md5String)\n\n\treturn deviceSign\n}\n\nfunc BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {\n\tdeviceSign := generateDeviceSign(deviceID, packageName)\n\tvar sb strings.Builder\n\n\tsb.WriteString(fmt.Sprintf(\"ANDROID-%s/%s \", appName, clientVersion))\n\tsb.WriteString(\"protocolVersion/200 \")\n\tsb.WriteString(\"accesstype/ \")\n\tsb.WriteString(fmt.Sprintf(\"clientid/%s \", clientID))\n\tsb.WriteString(fmt.Sprintf(\"clientversion/%s \", clientVersion))\n\tsb.WriteString(\"action_type/ \")\n\tsb.WriteString(\"networktype/WIFI \")\n\tsb.WriteString(\"sessionid/ \")\n\tsb.WriteString(fmt.Sprintf(\"deviceid/%s \", deviceID))\n\tsb.WriteString(\"providername/NONE \")\n\tsb.WriteString(fmt.Sprintf(\"devicesign/%s \", deviceSign))\n\tsb.WriteString(\"refresh_token/ \")\n\tsb.WriteString(fmt.Sprintf(\"sdkversion/%s \", sdkVersion))\n\tsb.WriteString(fmt.Sprintf(\"datetime/%d \", time.Now().UnixMilli()))\n\tsb.WriteString(fmt.Sprintf(\"usrno/%s \", userID))\n\tsb.WriteString(fmt.Sprintf(\"appname/android-%s \", appName))\n\tsb.WriteString(fmt.Sprintf(\"session_origin/ \"))\n\tsb.WriteString(fmt.Sprintf(\"grant_type/ \"))\n\tsb.WriteString(fmt.Sprintf(\"appid/ \"))\n\tsb.WriteString(fmt.Sprintf(\"clientip/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicename/Xiaomi_M2004j7ac \"))\n\tsb.WriteString(fmt.Sprintf(\"osversion/13 \"))\n\tsb.WriteString(fmt.Sprintf(\"platformversion/10 \"))\n\tsb.WriteString(fmt.Sprintf(\"accessmode/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicemodel/M2004J7AC \"))\n\n\treturn sb.String()\n}\n\n// RefreshCaptchaToken 刷新验证码token\nfunc (d *PikPakShare) RefreshCaptchaToken(action, userID string) error {\n\tmetas := map[string]string{\n\t\t\"client_version\": d.ClientVersion,\n\t\t\"package_name\":   d.PackageName,\n\t\t\"user_id\":        userID,\n\t}\n\tmetas[\"timestamp\"], metas[\"captcha_sign\"] = d.Common.GetCaptchaSign()\n\treturn d.refreshCaptchaToken(action, metas)\n}\n\n// GetCaptchaSign 获取验证码签名\nfunc (c *Common) GetCaptchaSign() (timestamp, sign string) {\n\ttimestamp = fmt.Sprint(time.Now().UnixMilli())\n\tstr := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)\n\tfor _, algorithm := range c.Algorithms {\n\t\tstr = utils.GetMD5EncodeStr(str + algorithm)\n\t}\n\tsign = \"1.\" + str\n\treturn\n}\n\n// refreshCaptchaToken 刷新CaptchaToken\nfunc (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {\n\tparam := CaptchaTokenRequest{\n\t\tAction:       action,\n\t\tCaptchaToken: d.GetCaptchaToken(),\n\t\tClientID:     d.ClientID,\n\t\tDeviceID:     d.GetDeviceID(),\n\t\tMeta:         metas,\n\t}\n\tvar e ErrResp\n\tvar resp CaptchaTokenResponse\n\t_, err := d.request(\"https://user.mypikpak.net/v1/shield/captcha/init\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetError(&e).SetBody(param)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif e.IsError() {\n\t\treturn errors.New(e.Error())\n\t}\n\n\t//if resp.Url != \"\" {\n\t//\treturn fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, resp.Url)\n\t//}\n\n\tif d.Common.RefreshCTokenCk != nil {\n\t\td.Common.RefreshCTokenCk(resp.CaptchaToken)\n\t}\n\td.Common.SetCaptchaToken(resp.CaptchaToken)\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/proton_drive/driver.go",
    "content": "package protondrive\n\n/*\nPackage protondrive\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-18\n\nThanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge\n\nThe power of open-source, the force of teamwork and the magic of reverse engineering!\n\n\nD@' 3z K!7 - The King Of Cracking\n\nДа здравствует Родина))\n*/\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/ProtonMail/gopenpgp/v2/crypto\"\n\tproton_api_bridge \"github.com/henrybear327/Proton-API-Bridge\"\n\t\"github.com/henrybear327/Proton-API-Bridge/common\"\n\t\"github.com/henrybear327/go-proton-api\"\n)\n\ntype ProtonDrive struct {\n\tmodel.Storage\n\tAddition\n\n\tprotonDrive *proton_api_bridge.ProtonDrive\n\n\tapiBase    string\n\tappVersion string\n\tprotonJson string\n\tuserAgent  string\n\tsdkVersion string\n\twebDriveAV string\n\n\tc *proton.Client\n\n\t// userKR   *crypto.KeyRing\n\taddrKRs  map[string]*crypto.KeyRing\n\taddrData map[string]proton.Address\n\n\tMainShare *proton.Share\n\n\tDefaultAddrKR *crypto.KeyRing\n\tMainShareKR   *crypto.KeyRing\n}\n\nfunc (d *ProtonDrive) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *ProtonDrive) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *ProtonDrive) Init(ctx context.Context) (err error) {\n\tdefer func() {\n\t\tif r := recover(); err == nil && r != nil {\n\t\t\terr = fmt.Errorf(\"ProtonDrive initialization panic: %v\", r)\n\t\t}\n\t}()\n\n\tif d.Email == \"\" {\n\t\treturn fmt.Errorf(\"email is required\")\n\t}\n\tif d.Password == \"\" {\n\t\treturn fmt.Errorf(\"password is required\")\n\t}\n\n\tconfig := &common.Config{\n\t\tAppVersion: d.appVersion,\n\t\tUserAgent:  d.userAgent,\n\t\tFirstLoginCredential: &common.FirstLoginCredentialData{\n\t\t\tUsername: d.Email,\n\t\t\tPassword: d.Password,\n\t\t\tTwoFA:    d.TwoFACode,\n\t\t},\n\t\tEnableCaching:              true,\n\t\tConcurrentBlockUploadCount: setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers),\n\t\t//ConcurrentFileCryptoCount:  2,\n\t\tUseReusableLogin:     d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}),\n\t\tReplaceExistingDraft: true,\n\t\tReusableCredential:   &d.ReusableCredential,\n\t}\n\n\tprotonDrive, _, err := proton_api_bridge.NewProtonDrive(\n\t\tctx,\n\t\tconfig,\n\t\td.authHandler,\n\t\tfunc() {},\n\t)\n\n\tif err != nil && config.UseReusableLogin {\n\t\tconfig.UseReusableLogin = false\n\t\tprotonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx,\n\t\t\tconfig,\n\t\t\td.authHandler,\n\t\t\tfunc() {},\n\t\t)\n\t\tif err == nil {\n\t\t\top.MustSaveDriverStorage(d)\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize ProtonDrive: %w\", err)\n\t}\n\n\tif err := d.initClient(ctx); err != nil {\n\t\treturn err\n\t}\n\n\td.protonDrive = protonDrive\n\td.MainShare = protonDrive.MainShare\n\tif d.RootFolderID == \"root\" || d.RootFolderID == \"\" {\n\t\td.RootFolderID = protonDrive.RootLink.LinkID\n\t}\n\td.MainShareKR = protonDrive.MainShareKR\n\td.DefaultAddrKR = protonDrive.DefaultAddrKR\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tentries, err := d.protonDrive.ListDirectory(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list directory: %w\", err)\n\t}\n\n\tobjects := make([]model.Obj, 0, len(entries))\n\tfor _, entry := range entries {\n\t\tobj := &model.Object{\n\t\t\tID:       entry.Link.LinkID,\n\t\t\tName:     entry.Name,\n\t\t\tSize:     entry.Link.Size,\n\t\t\tModified: time.Unix(entry.Link.ModifyTime, 0),\n\t\t\tIsFolder: entry.IsFolder,\n\t\t}\n\t\tobjects = append(objects, obj)\n\t}\n\n\treturn objects, nil\n}\n\nfunc (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tlink, err := d.getLink(ctx, file.GetID())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed get file link: %+v\", err)\n\t}\n\tfileSystemAttrs, err := d.protonDrive.GetActiveRevisionAttrs(ctx, link)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed get file revision: %+v\", err)\n\t}\n\t// 解密后的文件大小\n\tsize := fileSystemAttrs.Size\n\n\trangeReaderFunc := func(rangeCtx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tlength := httpRange.Length\n\t\tif length < 0 || httpRange.Start+length > size {\n\t\t\tlength = size - httpRange.Start\n\t\t}\n\t\treader, _, _, err := d.protonDrive.DownloadFile(rangeCtx, link, httpRange.Start)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed start download: %+v\", err)\n\t\t}\n\t\treturn utils.ReadCloser{\n\t\t\tReader: io.LimitReader(reader, length),\n\t\t\tCloser: reader,\n\t\t}, nil\n\t}\n\n\texpiration := time.Minute\n\treturn &model.Link{\n\t\tRangeReader:   stream.RateLimitRangeReaderFunc(rangeReaderFunc),\n\t\tContentLength: size,\n\t\tExpiration:    &expiration,\n\t}, nil\n}\n\nfunc (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tid, err := d.protonDrive.CreateNewFolderByID(ctx, parentDir.GetID(), dirName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create directory: %w\", err)\n\t}\n\n\tnewDir := &model.Object{\n\t\tID:       id,\n\t\tName:     dirName,\n\t\tIsFolder: true,\n\t\tModified: time.Now(),\n\t}\n\treturn newDir, nil\n}\n\nfunc (d *ProtonDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn d.DirectMove(ctx, srcObj, dstDir)\n}\n\nfunc (d *ProtonDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif d.protonDrive == nil {\n\t\treturn nil, fmt.Errorf(\"protonDrive bridge is nil\")\n\t}\n\n\treturn d.DirectRename(ctx, srcObj, newName)\n}\n\nfunc (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\treturn nil, fmt.Errorf(\"directory copy not supported\")\n\t}\n\n\tsrcLink, err := d.getLink(ctx, srcObj.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treader, linkSize, fileSystemAttrs, err := d.protonDrive.DownloadFile(ctx, srcLink, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to download source file: %w\", err)\n\t}\n\tdefer reader.Close()\n\n\tactualSize := linkSize\n\tif fileSystemAttrs != nil && fileSystemAttrs.Size > 0 {\n\t\tactualSize = fileSystemAttrs.Size\n\t}\n\n\tfile := &stream.FileStream{\n\t\tCtx: ctx,\n\t\tObj: &model.Object{\n\t\t\tName: srcObj.GetName(),\n\t\t\t// Use the accurate and real size\n\t\t\tSize:     actualSize,\n\t\t\tModified: srcObj.ModTime(),\n\t\t},\n\t\tReader: reader,\n\t}\n\tdefer file.Close()\n\treturn d.Put(ctx, dstDir, file, func(percentage float64) {})\n}\n\nfunc (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error {\n\tif obj.IsDir() {\n\t\treturn d.protonDrive.MoveFolderToTrashByID(ctx, obj.GetID(), false)\n\t} else {\n\t\treturn d.protonDrive.MoveFileToTrashByID(ctx, obj.GetID())\n\t}\n}\n\nfunc (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn d.uploadFile(ctx, dstDir.GetID(), file, up)\n}\n\nfunc (d *ProtonDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tabout, err := d.protonDrive.About(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: about.MaxSpace,\n\t\t\tUsedSpace:  about.UsedSpace,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*ProtonDrive)(nil)\n"
  },
  {
    "path": "drivers/proton_drive/meta.go",
    "content": "package protondrive\n\n/*\nPackage protondrive\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-18\n\nThanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge\n\nThe power of open-source, the force of teamwork and the magic of reverse engineering!\n\n\nD@' 3z K!7 - The King Of Cracking\n\nДа здравствует Родина))\n*/\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/henrybear327/Proton-API-Bridge/common\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tEmail              string `json:\"email\" required:\"true\" type:\"string\"`\n\tPassword           string `json:\"password\" required:\"true\" type:\"string\"`\n\tTwoFACode          string `json:\"two_fa_code\" type:\"string\"`\n\tChunkSize          int64  `json:\"chunk_size\" type:\"number\" default:\"100\"`\n\tUseReusableLogin   bool   `json:\"use_reusable_login\" type:\"bool\" default:\"true\" help:\"Use reusable login credentials instead of username/password\"`\n\tReusableCredential common.ReusableCredentialData\n}\n\nvar config = driver.Config{\n\tName:        \"ProtonDrive\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tDefaultRoot: \"root\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ProtonDrive{\n\t\t\tAddition: Addition{\n\t\t\t\tUseReusableLogin: true,\n\t\t\t},\n\t\t\tapiBase:    \"https://drive.proton.me/api\",\n\t\t\tappVersion: \"windows-drive@1.11.3+rclone+proton\",\n\t\t\tprotonJson: \"application/vnd.protonmail.v1+json\",\n\t\t\tsdkVersion: \"js@0.3.0\",\n\t\t\tuserAgent:  \"ProtonDrive/v1.70.0 (Windows NT 10.0.22000; Win64; x64)\",\n\t\t\twebDriveAV: \"web-drive@5.2.0+0f69f7a8\",\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/proton_drive/types.go",
    "content": "package protondrive\n\n/*\nPackage protondrive\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-18\n\nThanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge\n\nThe power of open-source, the force of teamwork and the magic of reverse engineering!\n\n\nD@' 3z K!7 - The King Of Cracking\n\nДа здравствует Родина))\n*/\n\ntype MoveRequest struct {\n\tParentLinkID            string  `json:\"ParentLinkID\"`\n\tNodePassphrase          string  `json:\"NodePassphrase\"`\n\tNodePassphraseSignature *string `json:\"NodePassphraseSignature\"`\n\tName                    string  `json:\"Name\"`\n\tNameSignatureEmail      string  `json:\"NameSignatureEmail\"`\n\tHash                    string  `json:\"Hash\"`\n\tOriginalHash            string  `json:\"OriginalHash\"`\n\tContentHash             *string `json:\"ContentHash\"` // Maybe null\n}\n\ntype RenameRequest struct {\n\tName               string `json:\"Name\"`               // PGP encrypted name\n\tNameSignatureEmail string `json:\"NameSignatureEmail\"` // User's signature email\n\tHash               string `json:\"Hash\"`               // New name hash\n\tOriginalHash       string `json:\"OriginalHash\"`       // Current name hash\n}\n\ntype RenameResponse struct {\n\tCode int `json:\"Code\"`\n}\n"
  },
  {
    "path": "drivers/proton_drive/util.go",
    "content": "package protondrive\n\n/*\nPackage protondrive\nAuthor: Da3zKi7<da3zki7@duck.com>\nDate: 2025-09-18\n\nThanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge\n\nThe power of open-source, the force of teamwork and the magic of reverse engineering!\n\n\nD@' 3z K!7 - The King Of Cracking\n\nДа здравствует Родина))\n*/\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/ProtonMail/gopenpgp/v2/crypto\"\n\t\"github.com/henrybear327/go-proton-api\"\n)\n\nfunc (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t_, err := d.getLink(ctx, parentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get parent link: %w\", err)\n\t}\n\n\tvar reader io.Reader\n\t// Use buffered reader with larger buffer for better performance\n\tvar bufferSize int\n\n\t// File > 100MB (default)\n\tif file.GetSize() > d.ChunkSize*1024*1024 {\n\t\t// 256KB for large files\n\t\tbufferSize = 256 * 1024\n\t\t// File > 10MB\n\t} else if file.GetSize() > 10*1024*1024 {\n\t\t// 128KB for medium files\n\t\tbufferSize = 128 * 1024\n\t} else {\n\t\t// 64KB for small files\n\t\tbufferSize = 64 * 1024\n\t}\n\n\t// reader = bufio.NewReader(file)\n\treader = bufio.NewReaderSize(file, bufferSize)\n\treader = &driver.ReaderUpdatingProgress{\n\t\tReader: &stream.SimpleReaderWithSize{\n\t\t\tReader: reader,\n\t\t\tSize:   file.GetSize(),\n\t\t},\n\t\tUpdateProgress: up,\n\t}\n\treader = driver.NewLimitedUploadStream(ctx, reader)\n\n\tid, _, err := d.protonDrive.UploadFileByReader(ctx, parentLinkID, file.GetName(), file.ModTime(), reader, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload file: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tID:       id,\n\t\tName:     file.GetName(),\n\t\tSize:     file.GetSize(),\n\t\tModified: file.ModTime(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\nfunc (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) {\n\tparentLink, err := d.getLink(ctx, parentLinkID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent link: %w\", err)\n\t}\n\n\t// Get parent node keyring\n\tparentNodeKR, err := d.getLinkKR(ctx, parentLink)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent keyring: %w\", err)\n\t}\n\n\t// Temporary file (request)\n\ttempReq := proton.CreateFileReq{\n\t\tSignatureAddress: d.MainShare.Creator,\n\t}\n\n\t// Encrypt the filename\n\terr = tempReq.SetName(name, d.DefaultAddrKR, parentNodeKR)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to encrypt filename: %w\", err)\n\t}\n\n\treturn tempReq.Name, nil\n}\n\nfunc (d *ProtonDrive) generateFileNameHash(ctx context.Context, name string, parentLinkID string) (string, error) {\n\tparentLink, err := d.getLink(ctx, parentLinkID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent link: %w\", err)\n\t}\n\n\t// Get parent node keyring\n\tparentNodeKR, err := d.getLinkKR(ctx, parentLink)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent keyring: %w\", err)\n\t}\n\n\tsignatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get signature verification keyring: %w\", err)\n\t}\n\n\tparentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent hash key: %w\", err)\n\t}\n\n\tnameHash, err := proton.GetNameHash(name, parentHashKey)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate name hash: %w\", err)\n\t}\n\n\treturn nameHash, nil\n}\n\nfunc (d *ProtonDrive) getOriginalNameHash(link *proton.Link) (string, error) {\n\tif link == nil {\n\t\treturn \"\", fmt.Errorf(\"link cannot be nil\")\n\t}\n\n\tif link.Hash == \"\" {\n\t\treturn \"\", fmt.Errorf(\"link hash is empty\")\n\t}\n\n\treturn link.Hash, nil\n}\n\nfunc (d *ProtonDrive) getLink(ctx context.Context, linkID string) (*proton.Link, error) {\n\tif linkID == \"\" {\n\t\treturn nil, fmt.Errorf(\"linkID cannot be empty\")\n\t}\n\n\tlink, err := d.c.GetLink(ctx, d.MainShare.ShareID, linkID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &link, nil\n}\n\nfunc (d *ProtonDrive) getLinkKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) {\n\tif link == nil {\n\t\treturn nil, fmt.Errorf(\"link cannot be nil\")\n\t}\n\n\t// Root Link or Root Dir\n\tif link.ParentLinkID == \"\" {\n\t\tsignatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn link.GetKeyRing(d.MainShareKR, signatureVerificationKR)\n\t}\n\n\t// Get parent keyring recursively\n\tparentLink, err := d.getLink(ctx, link.ParentLinkID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparentNodeKR, err := d.getLinkKR(ctx, parentLink)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsignatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn link.GetKeyRing(parentNodeKR, signatureVerificationKR)\n}\n\nvar (\n\tErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New(\"either keyPass or saltedKeyPass must be not nil\")\n\tErrFailedToUnlockUserKeys             = errors.New(\"failed to unlock user keys\")\n)\n\nfunc getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, []byte, error) {\n\tuser, err := c.GetUser(ctx)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, err\n\t}\n\t// fmt.Printf(\"user %#v\", user)\n\n\taddrsArr, err := c.GetAddresses(ctx)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, err\n\t}\n\t// fmt.Printf(\"addr %#v\", addr)\n\n\tif saltedKeyPass == nil {\n\t\tif keyPass == nil {\n\t\t\treturn nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil\n\t\t}\n\n\t\t// Due to limitations, salts are stored using cacheCredentialToFile\n\t\tsalts, err := c.GetSalts(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, nil, err\n\t\t}\n\t\t// fmt.Printf(\"salts %#v\", salts)\n\n\t\tsaltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, nil, err\n\t\t}\n\t\t// fmt.Printf(\"saltedKeyPass ok\")\n\t}\n\n\tuserKR, addrKRs, err := proton.Unlock(user, addrsArr, saltedKeyPass, nil)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, err\n\t} else if userKR.CountDecryptionEntities() == 0 {\n\t\treturn nil, nil, nil, nil, ErrFailedToUnlockUserKeys\n\t}\n\n\taddrs := make(map[string]proton.Address)\n\tfor _, addr := range addrsArr {\n\t\taddrs[addr.Email] = addr\n\t}\n\n\treturn userKR, addrKRs, addrs, saltedKeyPass, nil\n}\n\nfunc (d *ProtonDrive) getSignatureVerificationKeyring(emailAddresses []string, verificationAddrKRs ...*crypto.KeyRing) (*crypto.KeyRing, error) {\n\tret, err := crypto.NewKeyRing(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, emailAddress := range emailAddresses {\n\t\tif addr, ok := d.addrData[emailAddress]; ok {\n\t\t\tif addrKR, exists := d.addrKRs[addr.ID]; exists {\n\t\t\t\terr = d.addKeysFromKR(ret, addrKR)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, kr := range verificationAddrKRs {\n\t\terr = d.addKeysFromKR(ret, kr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif ret.CountEntities() == 0 {\n\t\treturn nil, fmt.Errorf(\"no keyring for signature verification\")\n\t}\n\n\treturn ret, nil\n}\n\nfunc (d *ProtonDrive) addKeysFromKR(kr *crypto.KeyRing, newKRs ...*crypto.KeyRing) error {\n\tfor i := range newKRs {\n\t\tfor _, key := range newKRs[i].GetKeys() {\n\t\t\terr := kr.AddKey(key)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\t// fmt.Printf(\"DEBUG DirectRename: path=%s, newName=%s\", srcObj.GetPath(), newName)\n\n\tif d.MainShare == nil || d.DefaultAddrKR == nil {\n\t\treturn nil, fmt.Errorf(\"missing required fields: MainShare=%v, DefaultAddrKR=%v\",\n\t\t\td.MainShare != nil, d.DefaultAddrKR != nil)\n\t}\n\n\tif d.protonDrive == nil {\n\t\treturn nil, fmt.Errorf(\"protonDrive bridge is nil\")\n\t}\n\n\tsrcLink, err := d.getLink(ctx, srcObj.GetID())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find source: %w\", err)\n\t}\n\n\tparentLinkID := srcLink.ParentLinkID\n\tif parentLinkID == \"\" {\n\t\treturn nil, fmt.Errorf(\"cannot rename root folder\")\n\t}\n\n\tencryptedName, err := d.encryptFileName(ctx, newName, parentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encrypt filename: %w\", err)\n\t}\n\n\tnewHash, err := d.generateFileNameHash(ctx, newName, parentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate new hash: %w\", err)\n\t}\n\n\toriginalHash, err := d.getOriginalNameHash(srcLink)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get original hash: %w\", err)\n\t}\n\n\trenameReq := RenameRequest{\n\t\tName:               encryptedName,\n\t\tNameSignatureEmail: d.MainShare.Creator,\n\t\tHash:               newHash,\n\t\tOriginalHash:       originalHash,\n\t}\n\n\terr = d.executeRenameAPI(ctx, srcLink.LinkID, renameReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rename API call failed: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tID:       srcLink.LinkID,\n\t\tName:     newName,\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: srcObj.ModTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req RenameRequest) error {\n\trenameURL := fmt.Sprintf(d.apiBase+\"/drive/v2/volumes/%s/links/%s/rename\",\n\t\td.MainShare.VolumeID, linkID)\n\n\treqBody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal rename request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, \"PUT\", renameURL, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create HTTP request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\thttpReq.Header.Set(\"Accept\", d.protonJson)\n\thttpReq.Header.Set(\"X-Pm-Appversion\", d.webDriveAV)\n\thttpReq.Header.Set(\"X-Pm-Drive-Sdk-Version\", d.sdkVersion)\n\thttpReq.Header.Set(\"X-Pm-Uid\", d.ReusableCredential.UID)\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+d.ReusableCredential.AccessToken)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute rename request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"rename failed with status %d\", resp.StatusCode)\n\t}\n\n\tvar renameResp RenameResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&renameResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode rename response: %w\", err)\n\t}\n\n\tif renameResp.Code != 1000 {\n\t\treturn fmt.Errorf(\"rename failed with code %d\", renameResp.Code)\n\t}\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req MoveRequest) error {\n\t// fmt.Printf(\"DEBUG Move Request - Name: %s\\n\", req.Name)\n\t// fmt.Printf(\"DEBUG Move Request - Hash: %s\\n\", req.Hash)\n\t// fmt.Printf(\"DEBUG Move Request - OriginalHash: %s\\n\", req.OriginalHash)\n\t// fmt.Printf(\"DEBUG Move Request - ParentLinkID: %s\\n\", req.ParentLinkID)\n\n\t// fmt.Printf(\"DEBUG Move Request - Name length: %d\\n\", len(req.Name))\n\t// fmt.Printf(\"DEBUG Move Request - NameSignatureEmail: %s\\n\", req.NameSignatureEmail)\n\t// fmt.Printf(\"DEBUG Move Request - ContentHash: %v\\n\", req.ContentHash)\n\t// fmt.Printf(\"DEBUG Move Request - NodePassphrase length: %d\\n\", len(req.NodePassphrase))\n\t// fmt.Printf(\"DEBUG Move Request - NodePassphraseSignature length: %d\\n\", len(req.NodePassphraseSignature))\n\n\t// fmt.Printf(\"DEBUG Move Request - SrcLinkID: %s\\n\", linkID)\n\t// fmt.Printf(\"DEBUG Move Request - DstParentLinkID: %s\\n\", req.ParentLinkID)\n\t// fmt.Printf(\"DEBUG Move Request - ShareID: %s\\n\", d.MainShare.ShareID)\n\n\tsrcLink, _ := d.getLink(ctx, linkID)\n\tif srcLink != nil && srcLink.ParentLinkID == req.ParentLinkID {\n\t\treturn fmt.Errorf(\"cannot move to same parent directory\")\n\t}\n\n\tmoveURL := fmt.Sprintf(d.apiBase+\"/drive/v2/volumes/%s/links/%s/move\",\n\t\td.MainShare.VolumeID, linkID)\n\n\treqBody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal move request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, \"PUT\", moveURL, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create HTTP request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+d.ReusableCredential.AccessToken)\n\thttpReq.Header.Set(\"Accept\", d.protonJson)\n\thttpReq.Header.Set(\"X-Pm-Appversion\", d.webDriveAV)\n\thttpReq.Header.Set(\"X-Pm-Drive-Sdk-Version\", d.sdkVersion)\n\thttpReq.Header.Set(\"X-Pm-Uid\", d.ReusableCredential.UID)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute move request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar moveResp RenameResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode move response: %w\", err)\n\t}\n\n\tif moveResp.Code != 1000 {\n\t\treturn fmt.Errorf(\"move operation failed with code: %d\", moveResp.Code)\n\t}\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) {\n\t// fmt.Printf(\"DEBUG DirectMove: srcPath=%s, dstPath=%s\", srcObj.GetPath(), dstDir.GetPath())\n\n\tsrcLink, err := d.getLink(ctx, srcObj.GetID())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find source: %w\", err)\n\t}\n\n\tdstParentLinkID := dstDir.GetID()\n\n\tif srcObj.IsDir() {\n\t\t// Check if destination is a descendant of source\n\t\tif err := d.checkCircularMove(ctx, srcLink.LinkID, dstParentLinkID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Encrypt the filename for the new location\n\tencryptedName, err := d.encryptFileName(ctx, srcObj.GetName(), dstParentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encrypt filename: %w\", err)\n\t}\n\n\tnewHash, err := d.generateNameHash(ctx, srcObj.GetName(), dstParentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate new hash: %w\", err)\n\t}\n\n\toriginalHash, err := d.getOriginalNameHash(srcLink)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get original hash: %w\", err)\n\t}\n\n\t// Re-encrypt node passphrase for new parent context\n\treencryptedPassphrase, err := d.reencryptNodePassphrase(ctx, srcLink, dstParentLinkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to re-encrypt node passphrase: %w\", err)\n\t}\n\n\tmoveReq := MoveRequest{\n\t\tParentLinkID:       dstParentLinkID,\n\t\tNodePassphrase:     reencryptedPassphrase,\n\t\tName:               encryptedName,\n\t\tNameSignatureEmail: d.MainShare.Creator,\n\t\tHash:               newHash,\n\t\tOriginalHash:       originalHash,\n\t\tContentHash:        nil,\n\n\t\t// *** Causes rejection ***\n\t\t/* NodePassphraseSignature: srcLink.NodePassphraseSignature, */\n\t}\n\n\t//fmt.Printf(\"DEBUG MoveRequest validation:\\n\")\n\t//fmt.Printf(\"  Name length: %d\\n\", len(moveReq.Name))\n\t//fmt.Printf(\"  Hash: %s\\n\", moveReq.Hash)\n\t//fmt.Printf(\"  OriginalHash: %s\\n\", moveReq.OriginalHash)\n\t//fmt.Printf(\"  NodePassphrase length: %d\\n\", len(moveReq.NodePassphrase))\n\t/* fmt.Printf(\"  NodePassphraseSignature length: %d\\n\", len(moveReq.NodePassphraseSignature)) */\n\t//fmt.Printf(\"  NameSignatureEmail: %s\\n\", moveReq.NameSignatureEmail)\n\n\terr = d.executeMoveAPI(ctx, srcLink.LinkID, moveReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"move API call failed: %w\", err)\n\t}\n\n\treturn &model.Object{\n\t\tID:       srcLink.LinkID,\n\t\tName:     srcObj.GetName(),\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: srcObj.ModTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *ProtonDrive) reencryptNodePassphrase(ctx context.Context, srcLink *proton.Link, dstParentLinkID string) (string, error) {\n\t// Get source parent link with metadata\n\tsrcParentLink, err := d.getLink(ctx, srcLink.ParentLinkID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get source parent link: %w\", err)\n\t}\n\n\t// Get source parent keyring using link object\n\tsrcParentKR, err := d.getLinkKR(ctx, srcParentLink)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get source parent keyring: %w\", err)\n\t}\n\n\t// Get destination parent link with metadata\n\tdstParentLink, err := d.getLink(ctx, dstParentLinkID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get destination parent link: %w\", err)\n\t}\n\n\t// Get destination parent keyring using link object\n\tdstParentKR, err := d.getLinkKR(ctx, dstParentLink)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get destination parent keyring: %w\", err)\n\t}\n\n\t// Re-encrypt the node passphrase from source parent context to destination parent context\n\treencryptedPassphrase, err := reencryptKeyPacket(srcParentKR, dstParentKR, d.DefaultAddrKR, srcLink.NodePassphrase)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to re-encrypt key packet: %w\", err)\n\t}\n\n\treturn reencryptedPassphrase, nil\n}\n\nfunc (d *ProtonDrive) generateNameHash(ctx context.Context, name string, parentLinkID string) (string, error) {\n\tparentLink, err := d.getLink(ctx, parentLinkID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent link: %w\", err)\n\t}\n\n\t// Get parent node keyring\n\tparentNodeKR, err := d.getLinkKR(ctx, parentLink)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent keyring: %w\", err)\n\t}\n\n\t// Get signature verification keyring\n\tsignatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get signature verification keyring: %w\", err)\n\t}\n\n\tparentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get parent hash key: %w\", err)\n\t}\n\n\tnameHash, err := proton.GetNameHash(name, parentHashKey)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate name hash: %w\", err)\n\t}\n\n\treturn nameHash, nil\n}\n\nfunc reencryptKeyPacket(srcKR, dstKR, _ *crypto.KeyRing, passphrase string) (string, error) { // addrKR (3)\n\toldSplitMessage, err := crypto.NewPGPSplitMessageFromArmored(passphrase)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsessionKey, err := srcKR.DecryptSessionKey(oldSplitMessage.KeyPacket)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tnewKeyPacket, err := dstKR.EncryptSessionKey(sessionKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tnewSplitMessage := crypto.NewPGPSplitMessage(newKeyPacket, oldSplitMessage.DataPacket)\n\n\treturn newSplitMessage.GetArmored()\n}\n\nfunc (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParentLinkID string) error {\n\tcurrentLinkID := dstParentLinkID\n\n\tfor currentLinkID != \"\" && currentLinkID != d.RootFolderID {\n\t\tif currentLinkID == srcLinkID {\n\t\t\treturn fmt.Errorf(\"cannot move folder into itself or its subfolder\")\n\t\t}\n\n\t\tcurrentLink, err := d.getLink(ctx, currentLinkID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcurrentLinkID = currentLink.ParentLinkID\n\t}\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) authHandler(auth proton.Auth) {\n\tif auth.AccessToken != d.ReusableCredential.AccessToken || auth.RefreshToken != d.ReusableCredential.RefreshToken {\n\t\td.ReusableCredential.UID = auth.UID\n\t\td.ReusableCredential.AccessToken = auth.AccessToken\n\t\td.ReusableCredential.RefreshToken = auth.RefreshToken\n\n\t\tif err := d.initClient(context.Background()); err != nil {\n\t\t\tfmt.Printf(\"ProtonDrive: failed to reinitialize client after auth refresh: %v\\n\", err)\n\t\t}\n\n\t\top.MustSaveDriverStorage(d)\n\t}\n}\n\nfunc (d *ProtonDrive) initClient(ctx context.Context) error {\n\tclientOptions := []proton.Option{\n\t\tproton.WithAppVersion(d.appVersion),\n\t\tproton.WithUserAgent(d.userAgent),\n\t}\n\tmanager := proton.New(clientOptions...)\n\td.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken)\n\n\tsaltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to decode salted key pass: %w\", err)\n\t}\n\n\t_, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get account keyrings: %w\", err)\n\t}\n\n\td.addrKRs = addrKRs\n\td.addrData = addrs\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/quark_open/driver.go",
    "content": "package quark_open\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype QuarkOpen struct {\n\tmodel.Storage\n\tAddition\n\tconfig driver.Config\n\tconf   Conf\n}\n\nfunc (d *QuarkOpen) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *QuarkOpen) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *QuarkOpen) Init(ctx context.Context) error {\n\tvar resp UserInfoResp\n\n\t_, err := d.request(ctx, \"/open/v1/user/info\", http.MethodGet, nil, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Data.UserID != \"\" {\n\t\td.conf.userId = resp.Data.UserID\n\t} else {\n\t\treturn errors.New(\"failed to get user ID\")\n\t}\n\n\treturn err\n}\n\nfunc (d *QuarkOpen) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *QuarkOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.GetFiles(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *QuarkOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdata := base.Json{\n\t\t\"fid\": file.GetID(),\n\t}\n\tvar resp FileLikeResp\n\t_, err := d.request(ctx, \"/open/v1/file/get_download_url\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL: resp.Data.DownloadURL,\n\t\tHeader: http.Header{\n\t\t\t\"Cookie\": []string{d.generateAuthCookie()},\n\t\t},\n\t\tConcurrency: 3,\n\t\tPartSize:    10 * utils.MB,\n\t}, nil\n}\n\nfunc (d *QuarkOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdata := base.Json{\n\t\t\"dir_path\": dirName,\n\t\t\"pdir_fid\": parentDir.GetID(),\n\t}\n\t_, err := d.request(ctx, \"/open/v1/dir\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\n\treturn err\n}\n\nfunc (d *QuarkOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := base.Json{\n\t\t\"action_type\": 1,\n\t\t\"fid_list\":    []string{srcObj.GetID()},\n\t\t\"to_pdir_fid\": dstDir.GetID(),\n\t}\n\t_, err := d.request(ctx, \"/open/v1/file/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\n\treturn err\n}\n\nfunc (d *QuarkOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tdata := base.Json{\n\t\t\"fid\":           srcObj.GetID(),\n\t\t\"file_name\":     newName,\n\t\t\"conflict_mode\": \"REUSE\",\n\t}\n\t_, err := d.request(ctx, \"/open/v1/file/rename\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\n\treturn err\n}\n\nfunc (d *QuarkOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *QuarkOpen) Remove(ctx context.Context, obj model.Obj) error {\n\tdata := base.Json{\n\t\t\"action_type\": 1,\n\t\t\"fid_list\":    []string{obj.GetID()},\n\t}\n\t_, err := d.request(ctx, \"/open/v1/file/delete\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\n\treturn err\n}\n\nfunc (d *QuarkOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tmd5Str, sha1Str := stream.GetHash().GetHash(utils.MD5), stream.GetHash().GetHash(utils.SHA1)\n\tvar (\n\t\tmd5  hash.Hash\n\t\tsha1 hash.Hash\n\t)\n\twriters := []io.Writer{}\n\tif len(md5Str) != utils.MD5.Width {\n\t\tmd5 = utils.MD5.NewFunc()\n\t\twriters = append(writers, md5)\n\t}\n\tif len(sha1Str) != utils.SHA1.Width {\n\t\tsha1 = utils.SHA1.NewFunc()\n\t\twriters = append(writers, sha1)\n\t}\n\n\tif len(writers) > 0 {\n\t\t_, err := stream.CacheFullAndWriter(&up, io.MultiWriter(writers...))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif md5 != nil {\n\t\t\tmd5Str = hex.EncodeToString(md5.Sum(nil))\n\t\t}\n\t\tif sha1 != nil {\n\t\t\tsha1Str = hex.EncodeToString(sha1.Sum(nil))\n\t\t}\n\t}\n\t// pre\n\tpre, err := d.upPre(ctx, stream, dstDir.GetID(), md5Str, sha1Str)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// 如果预上传已经完成，直接返回--秒传\n\tif pre.Data.Finish {\n\t\tup(100)\n\t\treturn nil\n\t}\n\n\t// get part info\n\tpartInfo := d._getPartInfo(stream, pre.Data.PartSize)\n\t// get upload url info\n\tupUrlInfo, err := d.upUrl(ctx, pre, partInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// part up\n\tss, err := streamPkg.NewStreamSectionReader(stream, int(pre.Data.PartSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttotal := stream.GetSize()\n\t// 用于存储每个分片的ETag，后续commit时需要\n\tetags := make([]string, 0, len(partInfo))\n\n\t// 遍历上传每个分片\n\tfor i := range len(upUrlInfo.UploadUrls) {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\n\t\toffset := int64(i) * pre.Data.PartSize\n\t\tsize := min(pre.Data.PartSize, total-offset)\n\t\trd, err := ss.GetSectionReader(offset, size)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(func() error {\n\t\t\trd.Seek(0, io.SeekStart)\n\t\t\tetag, err := d.upPart(ctx, upUrlInfo, i, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tetags = append(etags, etag)\n\t\t\treturn nil\n\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second))\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to upload part %d: %w\", i, err)\n\t\t}\n\n\t\tup(95 * float64(offset+size) / float64(total))\n\t}\n\n\tdefer up(100)\n\treturn d.upFinish(ctx, pre, partInfo, etags)\n}\n\nvar _ driver.Driver = (*QuarkOpen)(nil)\n"
  },
  {
    "path": "drivers/quark_open/meta.go",
    "content": "package quark_open\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"none,file_type,file_name,updated_at,created_at\" default:\"none\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tUseOnlineAPI   bool   `json:\"use_online_api\" default:\"true\"`\n\tAPIAddress     string `json:\"api_url_address\" default:\"https://api.oplist.org/quarkyun/renewapi\"`\n\tAccessToken    string `json:\"access_token\" required:\"false\" default:\"\"`\n\tRefreshToken   string `json:\"refresh_token\" required:\"true\"`\n\tAppID          string `json:\"app_id\" required:\"true\" help:\"Keep it empty if you don't have one\"`\n\tSignKey        string `json:\"sign_key\" required:\"true\" help:\"Keep it empty if you don't have one\"`\n}\n\ntype Conf struct {\n\tua     string\n\tapi    string\n\tuserId string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &QuarkOpen{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"QuarkOpen\",\n\t\t\t\tOnlyProxy:         true,\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tua:  \"go-resty/3.0.0-beta.1 (https://resty.dev)\",\n\t\t\t\tapi: \"https://open-api-drive.quark.cn\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/quark_open/types.go",
    "content": "package quark_open\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"time\"\n)\n\ntype Resp struct {\n\tCommonRsp\n\tErrno     int    `json:\"errno\"`\n\tErrorInfo string `json:\"error_info\"`\n}\n\ntype CommonRsp struct {\n\tStatus int    `json:\"status\"`\n\tReqID  string `json:\"req_id\"`\n}\n\ntype UserInfo struct {\n\tUserID    string `json:\"user_id\"`\n\tNickname  string `json:\"nickname\"`\n\tAvatarURL string `json:\"avatar_url\"`\n}\n\ntype UserInfoResp struct {\n\tCommonRsp\n\tData UserInfo `json:\"data\"`\n}\n\ntype RefreshTokenOnlineAPIResp struct {\n\tRefreshToken string `json:\"refresh_token\"`\n\tAccessToken  string `json:\"access_token\"`\n\tAppID        string `json:\"app_id\"`\n\tSignKey      string `json:\"sign_key\"`\n\tErrorMessage string `json:\"text\"`\n}\n\ntype File struct {\n\tFid          string `json:\"fid\"`\n\tParentFid    string `json:\"parent_fid\"`\n\tCategory     int64  `json:\"category\"`\n\tFileName     string `json:\"filename\"`\n\tSize         int64  `json:\"size\"`\n\tFileType     string `json:\"file_type\"`\n\tThumbnailURL string `json:\"thumbnail_url\"`\n\tContentHash  string `json:\"content_hash\"`\n\tCreatedAt    int64  `json:\"created_at\"`\n\tUpdatedAt    int64  `json:\"updated_at\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       f.Fid,\n\t\t\tName:     f.FileName,\n\t\t\tSize:     f.Size,\n\t\t\tModified: time.UnixMilli(f.UpdatedAt),\n\t\t\tIsFolder: f.FileType == \"0\",\n\t\t\tCtime:    time.UnixMilli(f.CreatedAt),\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.ThumbnailURL},\n\t}\n}\n\ntype QueryCursor struct {\n\tVersion string `json:\"version\"`\n\tToken   string `json:\"token\"`\n}\n\ntype FileListResp struct {\n\tCommonRsp\n\tData struct {\n\t\tFileList        []File      `json:\"file_list\"`\n\t\tLastPage        bool        `json:\"last_page\"`\n\t\tNextQueryCursor QueryCursor `json:\"next_query_cursor\"`\n\t} `json:\"data\"`\n}\n\ntype FileLikeResp struct {\n\tCommonRsp\n\tData struct {\n\t\tFid         string `json:\"fid\"`\n\t\tSize        int    `json:\"size\"`\n\t\tFileName    string `json:\"file_name\"`\n\t\tDownloadURL string `json:\"download_url\"`\n\t} `json:\"data\"`\n}\n\ntype UpPreResp struct {\n\tCommonRsp\n\tData struct {\n\t\tFinish        bool   `json:\"finish\"`\n\t\tTaskID        string `json:\"task_id\"`\n\t\tFid           string `json:\"fid\"`\n\t\tCommonHeaders struct {\n\t\t\tXOssContentSha256 string `json:\"X-Oss-Content-Sha256\"`\n\t\t\tXOssDate          string `json:\"X-Oss-Date\"`\n\t\t} `json:\"common_headers\"`\n\t\tUploadUrls []struct {\n\t\t\tPartNumber    int `json:\"part_number\"`\n\t\t\tSignatureInfo struct {\n\t\t\t\tAuthType  string `json:\"auth_type\"`\n\t\t\t\tSignature string `json:\"signature\"`\n\t\t\t} `json:\"signature_info\"`\n\t\t\tUploadURL string `json:\"upload_url\"`\n\t\t\tExpired   int64  `json:\"expired\"`\n\t\t} `json:\"upload_urls\"`\n\t\tPartSize int64 `json:\"part_size\"`\n\t} `json:\"data\"`\n}\n\ntype UpUrlInfo struct {\n\tUploadUrls []struct {\n\t\tPartNumber    int `json:\"part_number\"`\n\t\tPartSize      int `json:\"part_size\"`\n\t\tSignatureInfo struct {\n\t\t\tAuthType  string `json:\"auth_type\"`\n\t\t\tSignature string `json:\"signature\"`\n\t\t} `json:\"signature_info\"`\n\t\tUploadURL string `json:\"upload_url\"`\n\t} `json:\"upload_urls\"`\n\tCommonHeaders struct {\n\t\tXOssContentSha256 string `json:\"X-Oss-Content-Sha256\"`\n\t\tXOssDate          string `json:\"X-Oss-Date\"`\n\t} `json:\"common_headers\"`\n\tUploadID string `json:\"upload_id\"`\n}\n\ntype UpUrlResp struct {\n\tCommonRsp\n\tData UpUrlInfo `json:\"data\"`\n}\n\ntype UpFinishResp struct {\n\tCommonRsp\n\tData struct {\n\t\tTaskID     string `json:\"task_id\"`\n\t\tFid        string `json:\"fid\"`\n\t\tFinish     bool   `json:\"finish\"`\n\t\tPdirFid    string `json:\"pdir_fid\"`\n\t\tThumbnail  string `json:\"thumbnail\"`\n\t\tFormatType string `json:\"format_type\"`\n\t\tSize       int    `json:\"size\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/quark_open/util.go",
    "content": "package quark_open\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *QuarkOpen) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}, manualSign ...*ManualSign) ([]byte, error) {\n\tu := d.conf.api + pathname\n\n\tvar tm, token, reqID string\n\n\t// 检查是否手动传入签名参数\n\tif len(manualSign) > 0 && manualSign[0] != nil {\n\t\ttm = manualSign[0].Tm\n\t\ttoken = manualSign[0].Token\n\t\treqID = manualSign[0].ReqID\n\t} else {\n\t\t// 自动生成签名参数\n\t\ttm, token, reqID = d.generateReqSign(method, pathname, d.Addition.SignKey)\n\t}\n\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":          \"application/json, text/plain, */*\",\n\t\t\"User-Agent\":      d.conf.ua,\n\t\t\"x-pan-tm\":        tm,\n\t\t\"x-pan-token\":     token,\n\t\t\"x-pan-client-id\": d.Addition.AppID,\n\t})\n\treq.SetQueryParams(map[string]string{\n\t\t\"req_id\":       reqID,\n\t\t\"access_token\": d.Addition.AccessToken,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Resp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 判断 是否需要 刷新 access_token\n\tif e.Status == -1 && (e.Errno == 11001 || (e.Errno == 14001 && strings.Contains(e.ErrorInfo, \"access_token\"))) {\n\t\t// token 过期\n\t\terr = d.refreshToken()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tctx1, cancelFunc := context.WithTimeout(ctx, 10*time.Second)\n\t\tdefer cancelFunc()\n\t\treturn d.request(ctx1, pathname, method, callback, resp)\n\t}\n\n\tif e.Status >= 400 || e.Errno != 0 {\n\t\treturn nil, errors.New(e.ErrorInfo)\n\t}\n\n\treturn res.Body(), nil\n}\n\nfunc (d *QuarkOpen) GetFiles(ctx context.Context, parent string) ([]File, error) {\n\tfiles := make([]File, 0)\n\tvar queryCursor QueryCursor\n\n\tfor {\n\t\treqBody := map[string]interface{}{\n\t\t\t\"parent_fid\": parent,\n\t\t\t\"size\":       100,             // 默认每页100个文件\n\t\t\t\"sort\":       \"file_name:asc\", // 基本排序方式\n\t\t}\n\t\t// 如果有排序设置\n\t\tif d.OrderBy != \"none\" {\n\t\t\treqBody[\"sort\"] = d.OrderBy + \":\" + d.OrderDirection\n\t\t}\n\t\t// 设置查询游标（用于分页）\n\t\tif queryCursor.Token != \"\" {\n\t\t\treqBody[\"query_cursor\"] = queryCursor\n\t\t}\n\n\t\tvar resp FileListResp\n\t\t_, err := d.request(ctx, \"/open/v1/file/list\", http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(reqBody)\n\t\t}, &resp)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfiles = append(files, resp.Data.FileList...)\n\t\tif resp.Data.LastPage {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryCursor = resp.Data.NextQueryCursor\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *QuarkOpen) upPre(ctx context.Context, file model.FileStreamer, parentId, md5, sha1 string) (UpPreResp, error) {\n\t// 获取当前时间\n\tnow := time.Now()\n\t// 获取文件大小\n\tfileSize := file.GetSize()\n\n\t// 手动生成 x-pan-token\n\thttpMethod := \"POST\"\n\tapiPath := \"/open/v1/file/upload_pre\"\n\ttm, xPanToken, reqID := d.generateReqSign(httpMethod, apiPath, d.Addition.SignKey)\n\n\t// 生成proof相关字段，传入 x-pan-token\n\tproofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2, err := d.generateProof(file, xPanToken)\n\tif err != nil {\n\t\treturn UpPreResp{}, fmt.Errorf(\"failed to generate proof: %w\", err)\n\t}\n\n\tdata := base.Json{\n\t\t\"file_name\":       file.GetName(),\n\t\t\"size\":            fileSize,\n\t\t\"format_type\":     file.GetMimetype(),\n\t\t\"md5\":             md5,\n\t\t\"sha1\":            sha1,\n\t\t\"l_created_at\":    now.UnixMilli(),\n\t\t\"l_updated_at\":    now.UnixMilli(),\n\t\t\"pdir_fid\":        parentId,\n\t\t\"same_path_reuse\": true,\n\t\t\"proof_version\":   proofVersion,\n\t\t\"proof_seed1\":     proofSeed1,\n\t\t\"proof_seed2\":     proofSeed2,\n\t\t\"proof_code1\":     proofCode1,\n\t\t\"proof_code2\":     proofCode2,\n\t}\n\n\tvar resp UpPreResp\n\n\t// 使用手动生成的签名参数\n\tmanualSign := &ManualSign{\n\t\tTm:    tm,\n\t\tToken: xPanToken,\n\t\tReqID: reqID,\n\t}\n\n\t_, err = d.request(ctx, \"/open/v1/file/upload_pre\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp, manualSign)\n\n\treturn resp, err\n}\n\n// generateProof 生成夸克云盘文件上传的proof验证信息\nfunc (d *QuarkOpen) generateProof(file model.FileStreamer, xPanToken string) (proofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2 string, err error) {\n\t// 获取文件大小\n\tfileSize := file.GetSize()\n\t// 设置proof_version (固定为\"v1\")\n\tproofVersion = \"v1\"\n\t// 生成proof_seed1 - 算法: md5(userid+x-pan-token)\n\tproofSeed1 = d.generateProofSeed1(xPanToken)\n\t// 生成proof_seed2 - 算法: md5(fileSize)\n\tproofSeed2 = d.generateProofSeed2(fileSize)\n\t// 生成proof_code1和proof_code2\n\tproofCode1, err = d.generateProofCode(file, proofSeed1, fileSize)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", \"\", \"\", fmt.Errorf(\"failed to generate proof_code1: %w\", err)\n\t}\n\n\tproofCode2, err = d.generateProofCode(file, proofSeed2, fileSize)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", \"\", \"\", fmt.Errorf(\"failed to generate proof_code2: %w\", err)\n\t}\n\n\treturn proofVersion, proofSeed1, proofSeed2, proofCode1, proofCode2, nil\n}\n\n// generateProofSeed1 生成proof_seed1，基于 userId、x-pan-token\nfunc (d *QuarkOpen) generateProofSeed1(xPanToken string) string {\n\tconcatString := d.conf.userId + xPanToken\n\tmd5Hash := md5.Sum([]byte(concatString))\n\treturn hex.EncodeToString(md5Hash[:])\n}\n\n// generateProofSeed2 生成proof_seed2，基于 fileSize\nfunc (d *QuarkOpen) generateProofSeed2(fileSize int64) string {\n\tmd5Hash := md5.Sum([]byte(strconv.FormatInt(fileSize, 10)))\n\treturn hex.EncodeToString(md5Hash[:])\n}\n\ntype ProofRange struct {\n\tStart int64\n\tEnd   int64\n}\n\n// generateProofCode 根据proof_seed和文件大小生成proof_code\nfunc (d *QuarkOpen) generateProofCode(file model.FileStreamer, proofSeed string, fileSize int64) (string, error) {\n\t// 获取读取范围\n\tproofRange, err := d.getProofRange(proofSeed, fileSize)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get proof range: %w\", err)\n\t}\n\n\t// 计算需要读取的长度\n\tlength := proofRange.End - proofRange.Start\n\tif length == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// 使用FileStreamer的RangeRead方法读取特定范围的数据\n\treader, err := file.RangeRead(http_range.Range{\n\t\tStart:  proofRange.Start,\n\t\tLength: length,\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to range read: %w\", err)\n\t}\n\tdefer func() {\n\t\tif closer, ok := reader.(io.Closer); ok {\n\t\t\tcloser.Close()\n\t\t}\n\t}()\n\n\t// 读取数据\n\tbuf := make([]byte, length)\n\tn, err := io.ReadFull(reader, buf)\n\tif n != int(length) {\n\t\treturn \"\", fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\n\t// Base64编码\n\treturn base64.StdEncoding.EncodeToString(buf), nil\n}\n\n// getProofRange 根据proof_seed和文件大小计算需要读取的文件范围\nfunc (d *QuarkOpen) getProofRange(proofSeed string, fileSize int64) (*ProofRange, error) {\n\tif fileSize == 0 {\n\t\treturn &ProofRange{}, nil\n\t}\n\t// 对 proofSeed 进行 MD5 处理，取前16个字符\n\tmd5Hash := md5.Sum([]byte(proofSeed))\n\ttmpStr := hex.EncodeToString(md5Hash[:])[:16]\n\t// 转为 uint64\n\ttmpInt, err := strconv.ParseUint(tmpStr, 16, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse hex string: %w\", err)\n\t}\n\t// 计算索引位置\n\tindex := tmpInt % uint64(fileSize)\n\n\tpr := &ProofRange{\n\t\tStart: int64(index),\n\t\tEnd:   int64(index) + 8,\n\t}\n\t// 确保 End 不超过文件大小\n\tif pr.End > fileSize {\n\t\tpr.End = fileSize\n\t}\n\n\treturn pr, nil\n}\n\nfunc (d *QuarkOpen) _getPartInfo(stream model.FileStreamer, partSize int64) []base.Json {\n\t// 计算分片信息\n\tpartInfo := make([]base.Json, 0)\n\ttotal := stream.GetSize()\n\tleft := total\n\tpartNumber := 1\n\n\t// 计算每个分片的大小和编号\n\tfor left > 0 {\n\t\tsize := partSize\n\t\tif left < partSize {\n\t\t\tsize = left\n\t\t}\n\n\t\tpartInfo = append(partInfo, base.Json{\n\t\t\t\"part_number\": partNumber,\n\t\t\t\"part_size\":   size,\n\t\t})\n\n\t\tleft -= size\n\t\tpartNumber++\n\t}\n\n\treturn partInfo\n}\n\nfunc (d *QuarkOpen) upUrl(ctx context.Context, pre UpPreResp, partInfo []base.Json) (upUrlInfo UpUrlInfo, err error) {\n\t// 构建请求体\n\tdata := base.Json{\n\t\t\"task_id\":        pre.Data.TaskID,\n\t\t\"part_info_list\": partInfo,\n\t}\n\tvar resp UpUrlResp\n\n\t_, err = d.request(ctx, \"/open/v1/file/get_upload_urls\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn upUrlInfo, err\n\t}\n\n\treturn resp.Data, nil\n\n}\n\nfunc (d *QuarkOpen) upPart(ctx context.Context, upUrlInfo UpUrlInfo, partNumber int, bytes io.Reader) (string, error) {\n\t// 创建请求\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, upUrlInfo.UploadUrls[partNumber].UploadURL, bytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Set(\"Authorization\", upUrlInfo.UploadUrls[partNumber].SignatureInfo.Signature)\n\treq.Header.Set(\"X-Oss-Date\", upUrlInfo.CommonHeaders.XOssDate)\n\treq.Header.Set(\"X-Oss-Content-Sha256\", upUrlInfo.CommonHeaders.XOssContentSha256)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"User-Agent\", \"Go-http-client/1.1\")\n\n\t// 发送请求\n\tresp, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn \"\", fmt.Errorf(\"up status: %d, error: %s\", resp.StatusCode, string(body))\n\t}\n\t// 返回 Etag 作为分片上传的标识\n\treturn resp.Header.Get(\"Etag\"), nil\n}\n\nfunc (d *QuarkOpen) upFinish(ctx context.Context, pre UpPreResp, partInfo []base.Json, etags []string) error {\n\t// 创建 part_info_list\n\tpartInfoList := make([]base.Json, len(partInfo))\n\t// 确保 partInfo 和 etags 长度一致\n\tif len(partInfo) != len(etags) {\n\t\treturn fmt.Errorf(\"part info count (%d) does not match etags count (%d)\", len(partInfo), len(etags))\n\t}\n\t// 组合 part_info_list\n\tfor i, part := range partInfo {\n\t\tpartInfoList[i] = base.Json{\n\t\t\t\"part_number\": part[\"part_number\"],\n\t\t\t\"part_size\":   part[\"part_size\"],\n\t\t\t\"etag\":        etags[i],\n\t\t}\n\t}\n\t// 构建请求体\n\tdata := base.Json{\n\t\t\"task_id\":        pre.Data.TaskID,\n\t\t\"part_info_list\": partInfoList,\n\t}\n\n\t// 发送请求\n\tvar resp UpFinishResp\n\t_, err := d.request(ctx, \"/open/v1/file/upload_finish\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Data.Finish != true {\n\t\treturn fmt.Errorf(\"upload finish failed, task_id: %s\", resp.Data.TaskID)\n\t}\n\n\treturn nil\n}\n\n// ManualSign 用于手动签名URL的结构体\ntype ManualSign struct {\n\tTm    string\n\tToken string\n\tReqID string\n}\n\nfunc (d *QuarkOpen) generateReqSign(method string, pathname string, signKey string) (string, string, string) {\n\t// 生成时间戳 (13位毫秒级)\n\ttimestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)\n\n\t// 生成 x-pan-token token的组成是: method + \"&\" + pathname + \"&\" + timestamp + \"&\" + signKey\n\ttokenData := method + \"&\" + pathname + \"&\" + timestamp + \"&\" + signKey\n\ttokenHash := sha256.Sum256([]byte(tokenData))\n\txPanToken := hex.EncodeToString(tokenHash[:])\n\n\t// 生成 req_id\n\treqUuid, _ := uuid.NewRandom()\n\treqID := reqUuid.String()\n\n\treturn timestamp, xPanToken, reqID\n}\n\nfunc (d *QuarkOpen) refreshToken() error {\n\trefresh, access, err := d._refreshToken()\n\tfor i := 0; i < 3; i++ {\n\t\tif err == nil {\n\t\t\tbreak\n\t\t} else {\n\t\t\tlog.Errorf(\"[quark_open] failed to refresh token: %s\", err)\n\t\t}\n\t\trefresh, access, err = d._refreshToken()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Infof(\"[quark_open] token exchange: %s -> %s\", d.RefreshToken, refresh)\n\td.RefreshToken, d.AccessToken = refresh, access\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *QuarkOpen) _refreshToken() (string, string, error) {\n\tif d.UseOnlineAPI && d.APIAddress != \"\" {\n\t\tu := d.APIAddress\n\t\tvar resp RefreshTokenOnlineAPIResp\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"quarkyun_oa\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn \"\", \"\", fmt.Errorf(\"empty token returned from official API, a wrong refresh token may have been used\")\n\t\t}\n\t\treturn resp.RefreshToken, resp.AccessToken, nil\n\t}\n\n\t// TODO 本地刷新逻辑\n\treturn \"\", \"\", fmt.Errorf(\"local refresh token logic is not implemented yet, please use online API or contact the developer\")\n}\n\n// 生成认证 Cookie\nfunc (d *QuarkOpen) generateAuthCookie() string {\n\treturn fmt.Sprintf(\"x_pan_client_id=%s; x_pan_access_token=%s\",\n\t\td.Addition.AppID, d.Addition.AccessToken)\n}\n"
  },
  {
    "path": "drivers/quark_uc/driver.go",
    "content": "package quark\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype QuarkOrUC struct {\n\tmodel.Storage\n\tAddition\n\tconfig driver.Config\n\tconf   Conf\n}\n\nfunc (d *QuarkOrUC) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *QuarkOrUC) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *QuarkOrUC) Init(ctx context.Context) error {\n\t_, err := d.request(\"/config\", http.MethodGet, nil, nil)\n\tif err == nil {\n\t\tif d.AdditionVersion != 2 {\n\t\t\td.AdditionVersion = 2\n\t\t\tif !d.UseTransCodingAddress && len(d.DownProxyURL) == 0 {\n\t\t\t\td.WebProxy = true\n\t\t\t\td.WebdavPolicy = \"native_proxy\"\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *QuarkOrUC) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *QuarkOrUC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.GetFiles(dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *QuarkOrUC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf := file.(*File)\n\n\tif d.UseTransCodingAddress && d.config.Name == \"Quark\" && f.Category == 1 && f.Size > 0 {\n\t\treturn d.getTranscodingLink(file)\n\t}\n\n\treturn d.getDownloadLink(file)\n}\n\nfunc (d *QuarkOrUC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdata := base.Json{\n\t\t\"dir_init_lock\": false,\n\t\t\"dir_path\":      \"\",\n\t\t\"file_name\":     dirName,\n\t\t\"pdir_fid\":      parentDir.GetID(),\n\t}\n\t_, err := d.request(\"/file\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\tif err == nil {\n\t\ttime.Sleep(time.Second)\n\t}\n\treturn err\n}\n\nfunc (d *QuarkOrUC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := base.Json{\n\t\t\"action_type\":  1,\n\t\t\"exclude_fids\": []string{},\n\t\t\"filelist\":     []string{srcObj.GetID()},\n\t\t\"to_pdir_fid\":  dstDir.GetID(),\n\t}\n\t_, err := d.request(\"/file/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *QuarkOrUC) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tdata := base.Json{\n\t\t\"fid\":       srcObj.GetID(),\n\t\t\"file_name\": newName,\n\t}\n\t_, err := d.request(\"/file/rename\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *QuarkOrUC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *QuarkOrUC) Remove(ctx context.Context, obj model.Obj) error {\n\tdata := base.Json{\n\t\t\"action_type\":  1,\n\t\t\"exclude_fids\": []string{},\n\t\t\"filelist\":     []string{obj.GetID()},\n\t}\n\t_, err := d.request(\"/file/delete\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tmd5Str, sha1Str := stream.GetHash().GetHash(utils.MD5), stream.GetHash().GetHash(utils.SHA1)\n\tvar (\n\t\tmd5  hash.Hash\n\t\tsha1 hash.Hash\n\t)\n\twriters := []io.Writer{}\n\tif len(md5Str) != utils.MD5.Width {\n\t\tmd5 = utils.MD5.NewFunc()\n\t\twriters = append(writers, md5)\n\t}\n\tif len(sha1Str) != utils.SHA1.Width {\n\t\tsha1 = utils.SHA1.NewFunc()\n\t\twriters = append(writers, sha1)\n\t}\n\n\tif len(writers) > 0 {\n\t\t_, err := stream.CacheFullAndWriter(&up, io.MultiWriter(writers...))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif md5 != nil {\n\t\t\tmd5Str = hex.EncodeToString(md5.Sum(nil))\n\t\t}\n\t\tif sha1 != nil {\n\t\t\tsha1Str = hex.EncodeToString(sha1.Sum(nil))\n\t\t}\n\t}\n\t// pre\n\tpre, err := d.upPre(stream, dstDir.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\t// hash\n\tfinish, err := d.upHash(md5Str, sha1Str, pre.Data.TaskId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif finish {\n\t\tup(100)\n\t\treturn nil\n\t}\n\t// part up\n\tss, err := streamPkg.NewStreamSectionReader(stream, pre.Metadata.PartSize, &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttotal := stream.GetSize()\n\tpartSize := int64(pre.Metadata.PartSize)\n\tuploadNums := int((total + partSize - 1) / partSize)\n\tmd5s := make([]string, 0, uploadNums)\n\tfor partIndex := range uploadNums {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\toffset := int64(partIndex) * partSize\n\t\tsize := min(partSize, total-offset)\n\t\trd, err := ss.GetSectionReader(offset, size)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = retry.Do(func() error {\n\t\t\trd.Seek(0, io.SeekStart)\n\t\t\tm, err := d.upPart(ctx, pre, stream.GetMimetype(), partIndex+1, driver.NewLimitedUploadStream(ctx, rd))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif m == \"finish\" {\n\t\t\t\tup(100)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tmd5s = append(md5s, m)\n\t\t\treturn nil\n\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second))\n\t\tss.FreeSectionReader(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tup(95 * float64(offset+size) / float64(total))\n\t}\n\tup(97)\n\terr = d.upCommit(pre, md5s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer up(100)\n\treturn d.upFinish(pre)\n}\n\nfunc (d *QuarkOrUC) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tmemberInfo, err := d.memberInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: memberInfo.Data.TotalCapacity,\n\t\t\tUsedSpace:  memberInfo.Data.UseCapacity,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*QuarkOrUC)(nil)\n"
  },
  {
    "path": "drivers/quark_uc/meta.go",
    "content": "package quark\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tCookie string `json:\"cookie\" required:\"true\"`\n\tdriver.RootID\n\tOrderBy               string `json:\"order_by\" type:\"select\" options:\"none,file_type,file_name,updated_at\" default:\"none\"`\n\tOrderDirection        string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tUseTransCodingAddress bool   `json:\"use_transcoding_address\" help:\"You can watch the transcoded video and support 302 redirection\" required:\"true\" default:\"false\"`\n\tOnlyListVideoFile     bool   `json:\"only_list_video_file\" default:\"false\"`\n\tAdditionVersion       int\n}\n\ntype Conf struct {\n\tua      string\n\treferer string\n\tapi     string\n\tpr      string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &QuarkOrUC{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"Quark\",\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tua:      \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch\",\n\t\t\t\treferer: \"https://pan.quark.cn\",\n\t\t\t\tapi:     \"https://drive.quark.cn/1/clouddrive\",\n\t\t\t\tpr:      \"ucpro\",\n\t\t\t},\n\t\t}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &QuarkOrUC{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"UC\",\n\t\t\t\tOnlyProxy:         true,\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tua:      \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch\",\n\t\t\t\treferer: \"https://drive.uc.cn\",\n\t\t\t\tapi:     \"https://pc-api.uc.cn/1/clouddrive\",\n\t\t\t\tpr:      \"UCBrowser\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/quark_uc/types.go",
    "content": "package quark\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Resp struct {\n\tStatus  int    `json:\"status\"`\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\t// ReqId     string `json:\"req_id\"`\n\t// Timestamp int    `json:\"timestamp\"`\n}\n\nvar _ model.Obj = (*File)(nil)\n\ntype File struct {\n\tFid      string `json:\"fid\"`\n\tFileName string `json:\"file_name\"`\n\t// PdirFid      string `json:\"pdir_fid\"`\n\tCategory int `json:\"category\"`\n\t// FileType     int    `json:\"file_type\"`\n\tSize int64 `json:\"size\"`\n\t// FormatType   string `json:\"format_type\"`\n\t// Status       int    `json:\"status\"`\n\t// Tags         string `json:\"tags,omitempty\"`\n\tLCreatedAt int64 `json:\"l_created_at\"`\n\tLUpdatedAt int64 `json:\"l_updated_at\"`\n\t// NameSpace    int    `json:\"name_space\"`\n\t// IncludeItems int    `json:\"include_items,omitempty\"`\n\t// RiskType     int    `json:\"risk_type\"`\n\t// BackupSign   int    `json:\"backup_sign\"`\n\t// Duration     int    `json:\"duration\"`\n\t// FileSource   string `json:\"file_source\"`\n\tFile      bool  `json:\"file\"`\n\tCreatedAt int64 `json:\"created_at\"`\n\tUpdatedAt int64 `json:\"updated_at\"`\n\t// PrivateExtra struct {} `json:\"_private_extra\"`\n\t// ObjCategory string `json:\"obj_category,omitempty\"`\n\t// Thumbnail string `json:\"thumbnail,omitempty\"`\n}\n\nfunc fileToObj(f File) *model.Object {\n\treturn &model.Object{\n\t\tID:       f.Fid,\n\t\tName:     f.FileName,\n\t\tSize:     f.Size,\n\t\tModified: time.UnixMilli(f.UpdatedAt),\n\t\tCtime:    time.UnixMilli(f.CreatedAt),\n\t\tIsFolder: !f.File,\n\t}\n}\n\nfunc (f *File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f *File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f *File) ModTime() time.Time {\n\treturn time.UnixMilli(f.UpdatedAt)\n}\n\nfunc (f *File) CreateTime() time.Time {\n\treturn time.UnixMilli(f.CreatedAt)\n}\n\nfunc (f *File) IsDir() bool {\n\treturn !f.File\n}\n\nfunc (f *File) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *File) GetID() string {\n\treturn f.Fid\n}\n\nfunc (f *File) GetPath() string {\n\treturn \"\"\n}\n\ntype SortResp struct {\n\tResp\n\tData struct {\n\t\tList []File `json:\"list\"`\n\t} `json:\"data\"`\n\tMetadata struct {\n\t\tSize  int    `json:\"_size\"`\n\t\tPage  int    `json:\"_page\"`\n\t\tCount int    `json:\"_count\"`\n\t\tTotal int    `json:\"_total\"`\n\t\tWay   string `json:\"way\"`\n\t} `json:\"metadata\"`\n}\n\ntype DownResp struct {\n\tResp\n\tData []struct {\n\t\t// Fid          string `json:\"fid\"`\n\t\t// FileName     string `json:\"file_name\"`\n\t\t// PdirFid      string `json:\"pdir_fid\"`\n\t\t// Category     int    `json:\"category\"`\n\t\t// FileType     int    `json:\"file_type\"`\n\t\t// Size         int    `json:\"size\"`\n\t\t// FormatType   string `json:\"format_type\"`\n\t\t// Status       int    `json:\"status\"`\n\t\t// Tags         string `json:\"tags\"`\n\t\t// LCreatedAt   int64  `json:\"l_created_at\"`\n\t\t// LUpdatedAt   int64  `json:\"l_updated_at\"`\n\t\t// NameSpace    int    `json:\"name_space\"`\n\t\t// Thumbnail    string `json:\"thumbnail\"`\n\t\tDownloadUrl string `json:\"download_url\"`\n\t\t//Md5          string `json:\"md5\"`\n\t\t//RiskType     int    `json:\"risk_type\"`\n\t\t//RangeSize    int    `json:\"range_size\"`\n\t\t//BackupSign   int    `json:\"backup_sign\"`\n\t\t//ObjCategory  string `json:\"obj_category\"`\n\t\t//Duration     int    `json:\"duration\"`\n\t\t//FileSource   string `json:\"file_source\"`\n\t\t//File         bool   `json:\"file\"`\n\t\t//CreatedAt    int64  `json:\"created_at\"`\n\t\t//UpdatedAt    int64  `json:\"updated_at\"`\n\t\t//PrivateExtra struct {\n\t\t//} `json:\"_private_extra\"`\n\t} `json:\"data\"`\n\t//Metadata struct {\n\t//\tAcc2 string `json:\"acc2\"`\n\t//\tAcc1 string `json:\"acc1\"`\n\t//} `json:\"metadata\"`\n}\n\ntype TranscodingResp struct {\n\tResp\n\tData struct {\n\t\tDefaultResolution       string `json:\"default_resolution\"`\n\t\tOriginDefaultResolution string `json:\"origin_default_resolution\"`\n\t\tVideoList               []struct {\n\t\t\tResolution string `json:\"resolution\"`\n\t\t\tVideoInfo  struct {\n\t\t\t\tDuration int     `json:\"duration\"`\n\t\t\t\tSize     int64   `json:\"size\"`\n\t\t\t\tFormat   string  `json:\"format\"`\n\t\t\t\tWidth    int     `json:\"width\"`\n\t\t\t\tHeight   int     `json:\"height\"`\n\t\t\t\tBitrate  float64 `json:\"bitrate\"`\n\t\t\t\tCodec    string  `json:\"codec\"`\n\t\t\t\tFps      float64 `json:\"fps\"`\n\t\t\t\tRotate   int     `json:\"rotate\"`\n\t\t\t\tAudio    struct {\n\t\t\t\t\tDuration int     `json:\"duration\"`\n\t\t\t\t\tBitrate  float64 `json:\"bitrate\"`\n\t\t\t\t\tCodec    string  `json:\"codec\"`\n\t\t\t\t\tChannels int     `json:\"channels\"`\n\t\t\t\t} `json:\"audio\"`\n\t\t\t\tUpdateTime int64  `json:\"update_time\"`\n\t\t\t\tURL        string `json:\"url\"`\n\t\t\t\tResolution string `json:\"resolution\"`\n\t\t\t\tHlsType    string `json:\"hls_type\"`\n\t\t\t\tFinish     bool   `json:\"finish\"`\n\t\t\t\tResoultion string `json:\"resoultion\"`\n\t\t\t\tSuccess    bool   `json:\"success\"`\n\t\t\t} `json:\"video_info,omitempty\"`\n\t\t\t// Right          string `json:\"right\"`\n\t\t\t// MemberRight    string `json:\"member_right\"`\n\t\t\t// TransStatus    string `json:\"trans_status\"`\n\t\t\t// Accessable     bool   `json:\"accessable\"`\n\t\t\t// SupportsFormat string `json:\"supports_format\"`\n\t\t\t// VideoFuncType  string `json:\"video_func_type,omitempty\"`\n\t\t} `json:\"video_list\"`\n\t\t// AudioList []interface{} `json:\"audio_list\"`\n\t\tFileName  string `json:\"file_name\"`\n\t\tNameSpace int    `json:\"name_space\"`\n\t\tSize      int64  `json:\"size\"`\n\t\tThumbnail string `json:\"thumbnail\"`\n\t\t//LastPlayInfo struct {\n\t\t//\tTime int `json:\"time\"`\n\t\t//} `json:\"last_play_info\"`\n\t\t//SeekPreviewData struct {\n\t\t//\tTotalFrameCount    int `json:\"total_frame_count\"`\n\t\t//\tTotalSpriteCount   int `json:\"total_sprite_count\"`\n\t\t//\tFrameWidth         int `json:\"frame_width\"`\n\t\t//\tFrameHeight        int `json:\"frame_height\"`\n\t\t//\tSpriteRow          int `json:\"sprite_row\"`\n\t\t//\tSpriteColumn       int `json:\"sprite_column\"`\n\t\t//\tPreviewSpriteInfos []struct {\n\t\t//\t\tURL        string `json:\"url\"`\n\t\t//\t\tFrameCount int    `json:\"frame_count\"`\n\t\t//\t\tTimes      []int  `json:\"times\"`\n\t\t//\t} `json:\"preview_sprite_infos\"`\n\t\t//} `json:\"seek_preview_data\"`\n\t\t//ObjKey string `json:\"obj_key\"`\n\t\t//Meta   struct {\n\t\t//\tDuration int     `json:\"duration\"`\n\t\t//\tSize     int64   `json:\"size\"`\n\t\t//\tFormat   string  `json:\"format\"`\n\t\t//\tWidth    int     `json:\"width\"`\n\t\t//\tHeight   int     `json:\"height\"`\n\t\t//\tBitrate  float64 `json:\"bitrate\"`\n\t\t//\tCodec    string  `json:\"codec\"`\n\t\t//\tFps      float64 `json:\"fps\"`\n\t\t//\tRotate   int     `json:\"rotate\"`\n\t\t//} `json:\"meta\"`\n\t\t//PreloadLevel       int  `json:\"preload_level\"`\n\t\t//HasSeekPreviewData bool `json:\"has_seek_preview_data\"`\n\t} `json:\"data\"`\n}\n\ntype UpPreResp struct {\n\tResp\n\tData struct {\n\t\tTaskId    string `json:\"task_id\"`\n\t\tFinish    bool   `json:\"finish\"`\n\t\tUploadId  string `json:\"upload_id\"`\n\t\tObjKey    string `json:\"obj_key\"`\n\t\tUploadUrl string `json:\"upload_url\"`\n\t\tFid       string `json:\"fid\"`\n\t\tBucket    string `json:\"bucket\"`\n\t\tCallback  struct {\n\t\t\tCallbackUrl  string `json:\"callbackUrl\"`\n\t\t\tCallbackBody string `json:\"callbackBody\"`\n\t\t} `json:\"callback\"`\n\t\tFormatType string `json:\"format_type\"`\n\t\tSize       int    `json:\"size\"`\n\t\tAuthInfo   string `json:\"auth_info\"`\n\t} `json:\"data\"`\n\tMetadata struct {\n\t\tPartThread int    `json:\"part_thread\"`\n\t\tAcc2       string `json:\"acc2\"`\n\t\tAcc1       string `json:\"acc1\"`\n\t\tPartSize   int    `json:\"part_size\"` // 分片大小\n\t} `json:\"metadata\"`\n}\n\ntype HashResp struct {\n\tResp\n\tData struct {\n\t\tFinish     bool   `json:\"finish\"`\n\t\tFid        string `json:\"fid\"`\n\t\tThumbnail  string `json:\"thumbnail\"`\n\t\tFormatType string `json:\"format_type\"`\n\t} `json:\"data\"`\n\tMetadata struct{} `json:\"metadata\"`\n}\n\ntype UpAuthResp struct {\n\tResp\n\tData struct {\n\t\tAuthKey string        `json:\"auth_key\"`\n\t\tSpeed   int           `json:\"speed\"`\n\t\tHeaders []interface{} `json:\"headers\"`\n\t} `json:\"data\"`\n\tMetadata struct{} `json:\"metadata\"`\n}\n\ntype MemberResp struct {\n\tResp\n\tData struct {\n\t\tMemberType        string `json:\"member_type\"`\n\t\tCreatedAt         uint64 `json:\"created_at\"`\n\t\tSecretUseCapacity int64  `json:\"secret_use_capacity\"`\n\t\tUseCapacity       int64  `json:\"use_capacity\"`\n\t\tIsNewUser         bool   `json:\"is_new_user\"`\n\t\tMemberStatus      struct {\n\t\t\tVip      string `json:\"VIP\"`\n\t\t\tZVip     string `json:\"Z_VIP\"`\n\t\t\tMiniVip  string `json:\"MINI_VIP\"`\n\t\t\tSuperVip string `json:\"SUPER_VIP\"`\n\t\t} `json:\"member_status\"`\n\t\tSecretTotalCapacity int64 `json:\"secret_total_capacity\"`\n\t\tTotalCapacity       int64 `json:\"total_capacity\"`\n\t} `json:\"data\"`\n\tMetadata struct {\n\t\tRangeSize     int    `json:\"range_size\"`\n\t\tServerCurTime uint64 `json:\"server_cur_time\"`\n\t} `json:\"metadata\"`\n}\n"
  },
  {
    "path": "drivers/quark_uc/util.go",
    "content": "package quark\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cookie\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *QuarkOrUC) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tu := d.conf.api + pathname\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":  d.Cookie,\n\t\t\"Accept\":  \"application/json, text/plain, */*\",\n\t\t\"Referer\": d.conf.referer,\n\t})\n\treq.SetQueryParam(\"pr\", d.conf.pr)\n\treq.SetQueryParam(\"fr\", \"pc\")\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Resp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t__puus := cookie.GetCookie(res.Cookies(), \"__puus\")\n\tif __puus != nil {\n\t\td.Cookie = cookie.SetStr(d.Cookie, \"__puus\", __puus.Value)\n\t\top.MustSaveDriverStorage(d)\n\t}\n\n\tif d.UseTransCodingAddress && d.config.Name == \"Quark\" {\n\t\t__pus := cookie.GetCookie(res.Cookies(), \"__pus\")\n\t\tif __pus != nil {\n\t\t\td.Cookie = cookie.SetStr(d.Cookie, \"__pus\", __pus.Value)\n\t\t\top.MustSaveDriverStorage(d)\n\t\t}\n\t}\n\n\tif e.Status >= 400 || e.Code != 0 {\n\t\treturn nil, errors.New(e.Message)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *QuarkOrUC) GetFiles(parent string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tpage := 1\n\tsize := 100\n\tquery := map[string]string{\n\t\t\"pdir_fid\":             parent,\n\t\t\"_size\":                strconv.Itoa(size),\n\t\t\"_fetch_total\":         \"1\",\n\t\t\"fetch_all_file\":       \"1\",\n\t\t\"fetch_risk_file_name\": \"1\",\n\t}\n\tif d.OrderBy != \"none\" {\n\t\tquery[\"_sort\"] = \"file_type:asc,\" + d.OrderBy + \":\" + d.OrderDirection\n\t}\n\tfor {\n\t\tquery[\"_page\"] = strconv.Itoa(page)\n\t\tvar resp SortResp\n\t\t_, err := d.request(\"/file/sort\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, file := range resp.Data.List {\n\t\t\tfile.FileName = html.UnescapeString(file.FileName)\n\t\t\tif d.OnlyListVideoFile {\n\t\t\t\t// 开启后 只列出视频文件和文件夹\n\t\t\t\tif file.IsDir() || file.Category == 1 {\n\t\t\t\t\tfiles = append(files, &file)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfiles = append(files, &file)\n\t\t\t}\n\t\t}\n\n\t\tif page*size >= resp.Metadata.Total {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *QuarkOrUC) getDownloadLink(file model.Obj) (*model.Link, error) {\n\tdata := base.Json{\n\t\t\"fids\": []string{file.GetID()},\n\t}\n\tvar resp DownResp\n\tua := d.conf.ua\n\t_, err := d.request(\"/file/download\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"User-Agent\", ua).\n\t\t\tSetBody(data)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL: resp.Data[0].DownloadUrl,\n\t\tHeader: http.Header{\n\t\t\t\"Cookie\":     []string{d.Cookie},\n\t\t\t\"Referer\":    []string{d.conf.referer},\n\t\t\t\"User-Agent\": []string{ua},\n\t\t},\n\t\tConcurrency: 3,\n\t\tPartSize:    10 * utils.MB,\n\t}, nil\n}\n\nfunc (d *QuarkOrUC) getTranscodingLink(file model.Obj) (*model.Link, error) {\n\tdata := base.Json{\n\t\t\"fid\":         file.GetID(),\n\t\t\"resolutions\": \"low,normal,high,super,2k,4k\",\n\t\t\"supports\":    \"fmp4_av,m3u8,dolby_vision\",\n\t}\n\tvar resp TranscodingResp\n\tua := d.conf.ua\n\n\t_, err := d.request(\"/file/v2/play/project\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"User-Agent\", ua).\n\t\t\tSetBody(data)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, info := range resp.Data.VideoList {\n\t\tif info.VideoInfo.URL != \"\" {\n\t\t\treturn &model.Link{\n\t\t\t\tURL:           info.VideoInfo.URL,\n\t\t\t\tContentLength: info.VideoInfo.Size,\n\t\t\t\tConcurrency:   3,\n\t\t\t\tPartSize:      10 * utils.MB,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no link found\")\n}\n\nfunc (d *QuarkOrUC) upPre(file model.FileStreamer, parentId string) (UpPreResp, error) {\n\tnow := time.Now()\n\tdata := base.Json{\n\t\t\"ccp_hash_update\": true,\n\t\t\"dir_name\":        \"\",\n\t\t\"file_name\":       file.GetName(),\n\t\t\"format_type\":     file.GetMimetype(),\n\t\t\"l_created_at\":    now.UnixMilli(),\n\t\t\"l_updated_at\":    now.UnixMilli(),\n\t\t\"pdir_fid\":        parentId,\n\t\t\"size\":            file.GetSize(),\n\t\t//\"same_path_reuse\": true,\n\t}\n\tvar resp UpPreResp\n\t_, err := d.request(\"/file/upload/pre\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\treturn resp, err\n}\n\nfunc (d *QuarkOrUC) upHash(md5, sha1, taskId string) (bool, error) {\n\tdata := base.Json{\n\t\t\"md5\":     md5,\n\t\t\"sha1\":    sha1,\n\t\t\"task_id\": taskId,\n\t}\n\tlog.Debugf(\"hash: %+v\", data)\n\tvar resp HashResp\n\t_, err := d.request(\"/file/update/hash\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\treturn resp.Data.Finish, err\n}\n\nfunc (d *QuarkOrUC) upPart(ctx context.Context, pre UpPreResp, mineType string, partNumber int, bytes io.Reader) (string, error) {\n\t// func (driver QuarkOrUC) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) {\n\ttimeStr := time.Now().UTC().Format(http.TimeFormat)\n\tdata := base.Json{\n\t\t\"auth_info\": pre.Data.AuthInfo,\n\t\t\"auth_meta\": fmt.Sprintf(`PUT\n\n%s\n%s\nx-oss-date:%s\nx-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\n/%s/%s?partNumber=%d&uploadId=%s`,\n\t\t\tmineType, timeStr, timeStr, pre.Data.Bucket, pre.Data.ObjKey, partNumber, pre.Data.UploadId),\n\t\t\"task_id\": pre.Data.TaskId,\n\t}\n\tvar resp UpAuthResp\n\t_, err := d.request(\"/file/upload/auth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data).SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t//if partNumber == 1 {\n\t//\tfinish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)\n\t//\tif err != nil {\n\t//\t\treturn \"\", err\n\t//\t}\n\t//\tif finish {\n\t//\t\treturn \"finish\", nil\n\t//\t}\n\t//}\n\tu := fmt.Sprintf(\"https://%s.%s/%s\", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, u, bytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Authorization\", resp.Data.AuthKey)\n\treq.Header.Set(\"Content-Type\", mineType)\n\treq.Header.Set(\"Referer\", \"https://pan.quark.cn/\")\n\treq.Header.Set(\"x-oss-date\", timeStr)\n\treq.Header.Set(\"x-oss-user-agent\", \"aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\")\n\tq := req.URL.Query()\n\tq.Add(\"partNumber\", strconv.Itoa(partNumber))\n\tq.Add(\"uploadId\", pre.Data.UploadId)\n\treq.URL.RawQuery = q.Encode()\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != 200 {\n\t\trespBody, _ := io.ReadAll(res.Body)\n\t\treturn \"\", fmt.Errorf(\"up status: %d, error: %s\", res.StatusCode, string(respBody))\n\t}\n\treturn res.Header.Get(\"Etag\"), nil\n}\n\nfunc (d *QuarkOrUC) upCommit(pre UpPreResp, md5s []string) error {\n\ttimeStr := time.Now().UTC().Format(http.TimeFormat)\n\tlog.Debugf(\"md5s: %+v\", md5s)\n\tbodyBuilder := strings.Builder{}\n\tbodyBuilder.WriteString(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<CompleteMultipartUpload>\n`)\n\tfor i, m := range md5s {\n\t\tbodyBuilder.WriteString(fmt.Sprintf(`<Part>\n<PartNumber>%d</PartNumber>\n<ETag>%s</ETag>\n</Part>\n`, i+1, m))\n\t}\n\tbodyBuilder.WriteString(\"</CompleteMultipartUpload>\")\n\tbody := bodyBuilder.String()\n\tm := md5.New()\n\tm.Write([]byte(body))\n\tcontentMd5 := base64.StdEncoding.EncodeToString(m.Sum(nil))\n\tcallbackBytes, err := utils.Json.Marshal(pre.Data.Callback)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcallbackBase64 := base64.StdEncoding.EncodeToString(callbackBytes)\n\tdata := base.Json{\n\t\t\"auth_info\": pre.Data.AuthInfo,\n\t\t\"auth_meta\": fmt.Sprintf(`POST\n%s\napplication/xml\n%s\nx-oss-callback:%s\nx-oss-date:%s\nx-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\n/%s/%s?uploadId=%s`,\n\t\t\tcontentMd5, timeStr, callbackBase64, timeStr,\n\t\t\tpre.Data.Bucket, pre.Data.ObjKey, pre.Data.UploadId),\n\t\t\"task_id\": pre.Data.TaskId,\n\t}\n\tlog.Debugf(\"xml: %s\", body)\n\tlog.Debugf(\"auth data: %+v\", data)\n\tvar resp UpAuthResp\n\t_, err = d.request(\"/file/upload/auth\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu := fmt.Sprintf(\"https://%s.%s/%s\", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)\n\tres, err := base.RestyClient.R().\n\t\tSetHeaders(map[string]string{\n\t\t\t\"Authorization\":    resp.Data.AuthKey,\n\t\t\t\"Content-MD5\":      contentMd5,\n\t\t\t\"Content-Type\":     \"application/xml\",\n\t\t\t\"Referer\":          \"https://pan.quark.cn/\",\n\t\t\t\"x-oss-callback\":   callbackBase64,\n\t\t\t\"x-oss-date\":       timeStr,\n\t\t\t\"x-oss-user-agent\": \"aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\",\n\t\t}).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"uploadId\": pre.Data.UploadId,\n\t\t}).SetBody(body).Post(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn fmt.Errorf(\"up status: %d, error: %s\", res.StatusCode(), res.String())\n\t}\n\treturn nil\n}\n\nfunc (d *QuarkOrUC) upFinish(pre UpPreResp) error {\n\tdata := base.Json{\n\t\t\"obj_key\": pre.Data.ObjKey,\n\t\t\"task_id\": pre.Data.TaskId,\n\t}\n\t_, err := d.request(\"/file/upload/finish\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttime.Sleep(time.Second)\n\treturn nil\n}\n\nfunc (d *QuarkOrUC) memberInfo(ctx context.Context) (*MemberResp, error) {\n\tvar resp MemberResp\n\tquery := map[string]string{\n\t\t\"fetch_subscribe\": \"false\",\n\t\t\"_ch\":             \"home\",\n\t\t\"fetch_identity\":  \"false\",\n\t}\n\t_, err := d.request(\"/member\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(query)\n\t\treq.SetContext(ctx)\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/quark_uc_tv/driver.go",
    "content": "package quark_uc_tv\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype QuarkUCTV struct {\n\t*QuarkUCTVCommon\n\tmodel.Storage\n\tAddition\n\tconfig driver.Config\n\tconf   Conf\n}\n\nfunc (d *QuarkUCTV) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *QuarkUCTV) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *QuarkUCTV) Init(ctx context.Context) error {\n\n\tif d.Addition.DeviceID == \"\" {\n\t\td.Addition.DeviceID = utils.GetMD5EncodeStr(time.Now().String())\n\t}\n\top.MustSaveDriverStorage(d)\n\n\tif d.QuarkUCTVCommon == nil {\n\t\td.QuarkUCTVCommon = &QuarkUCTVCommon{\n\t\t\tAccessToken: \"\",\n\t\t}\n\t}\n\tctx1, cancelFunc := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancelFunc()\n\tif d.Addition.RefreshToken == \"\" {\n\t\tif d.Addition.QueryToken == \"\" {\n\t\t\tqrData, err := d.getLoginCode(ctx1)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 展示二维码\n\t\t\tqrTemplate := `<body>\n        <img src=\"data:image/jpeg;base64,%s\"/>\n    </body>`\n\t\t\tqrPage := fmt.Sprintf(qrTemplate, qrData)\n\t\t\treturn fmt.Errorf(\"need verify: \\n%s\", qrPage)\n\t\t} else {\n\t\t\t// 通过query token获取code -> refresh token\n\t\t\tcode, err := d.getCode(ctx1)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 通过code获取refresh token\n\t\t\terr = d.getRefreshTokenByTV(ctx1, code, false)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\t// 通过refresh token获取access token\n\tif d.QuarkUCTVCommon.AccessToken == \"\" {\n\t\terr := d.getRefreshTokenByTV(ctx1, d.Addition.RefreshToken, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 验证 access token 是否有效\n\t_, err := d.isLogin(ctx1)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *QuarkUCTV) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *QuarkUCTV) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tpageIndex := int64(0)\n\tpageSize := int64(100)\n\tdesc := \"1\"\n\torderBy := \"3\"\n\tif d.OrderDirection == \"asc\" {\n\t\tdesc = \"0\"\n\t}\n\tif d.OrderBy == \"file_name\" {\n\t\torderBy = \"1\"\n\t}\n\tfor {\n\t\tvar filesData FilesData\n\t\t_, err := d.request(ctx, \"/file\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"method\":     \"list\",\n\t\t\t\t\"parent_fid\": dir.GetID(),\n\t\t\t\t\"order_by\":   orderBy,\n\t\t\t\t\"desc\":       desc,\n\t\t\t\t\"category\":   \"\",\n\t\t\t\t\"source\":     \"\",\n\t\t\t\t\"ex_source\":  \"\",\n\t\t\t\t\"list_all\":   \"0\",\n\t\t\t\t\"page_size\":  strconv.FormatInt(pageSize, 10),\n\t\t\t\t\"page_index\": strconv.FormatInt(pageIndex, 10),\n\t\t\t})\n\t\t}, &filesData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := range filesData.Data.Files {\n\t\t\tfiles = append(files, &filesData.Data.Files[i])\n\t\t}\n\t\tif pageIndex*pageSize >= filesData.Data.TotalCount {\n\t\t\tbreak\n\t\t} else {\n\t\t\tpageIndex++\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf := file.(*Files)\n\n\tif d.Addition.VideoLinkMethod == \"streaming\" && f.Category == 1 && f.Size > 0 {\n\t\treturn d.getTranscodingLink(ctx, file)\n\t}\n\n\treturn d.getDownloadLink(ctx, file)\n}\n\nfunc (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *QuarkUCTV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *QuarkUCTV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *QuarkUCTV) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *QuarkUCTV) Remove(ctx context.Context, obj model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *QuarkUCTV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\ntype QuarkUCTVCommon struct {\n\tAccessToken string\n}\n\nvar _ driver.Driver = (*QuarkUCTV)(nil)\n"
  },
  {
    "path": "drivers/quark_uc_tv/meta.go",
    "content": "package quark_uc_tv\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootID\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"file_name,updated_at\" default:\"updated_at\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"desc\"`\n\t// define other\n\tRefreshToken string `json:\"refresh_token\" required:\"false\" default:\"\"`\n\t// 必要且影响登录,由签名决定\n\tDeviceID string `json:\"device_id\"  required:\"false\" default:\"\"`\n\t// 登陆所用的数据 无需手动填写\n\tQueryToken string `json:\"query_token\" required:\"false\" default:\"\" help:\"don't edit'\"`\n\t// 视频文件链接获取方式 download(可获取源视频) or streaming(获取转码后的视频)\n\tVideoLinkMethod string `json:\"link_method\" required:\"true\" type:\"select\" options:\"download,streaming\" default:\"download\"`\n}\n\ntype Conf struct {\n\tapi      string\n\tclientID string\n\tsignKey  string\n\tappVer   string\n\tchannel  string\n\tcodeApi  string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &QuarkUCTV{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"QuarkTV\",\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t\tNoUpload:          true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tapi:      \"https://open-api-drive.quark.cn\",\n\t\t\t\tclientID: \"d3194e61504e493eb6222857bccfed94\",\n\t\t\t\tsignKey:  \"kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d\",\n\t\t\t\tappVer:   \"1.8.2.2\",\n\t\t\t\tchannel:  \"GENERAL\",\n\t\t\t\tcodeApi:  \"http://api.extscreen.com/quarkdrive\",\n\t\t\t},\n\t\t}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &QuarkUCTV{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"UCTV\",\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tNoOverwriteUpload: true,\n\t\t\t\tNoUpload:          true,\n\t\t\t},\n\t\t\tconf: Conf{\n\t\t\t\tapi:      \"https://open-api-drive.uc.cn\",\n\t\t\t\tclientID: \"5acf882d27b74502b7040b0c65519aa7\",\n\t\t\t\tsignKey:  \"l3srvtd7p42l0d0x1u8d7yc8ye9kki4d\",\n\t\t\t\tappVer:   \"1.7.2.2\",\n\t\t\t\tchannel:  \"UCTVOFFICIALWEB\",\n\t\t\t\tcodeApi:  \"http://api.extscreen.com/ucdrive\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/quark_uc_tv/types.go",
    "content": "package quark_uc_tv\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Resp struct {\n\tCommonRsp\n\tErrno     int    `json:\"errno\"`\n\tErrorInfo string `json:\"error_info\"`\n}\n\ntype CommonRsp struct {\n\tStatus int    `json:\"status\"`\n\tReqID  string `json:\"req_id\"`\n}\n\ntype RefreshTokenAuthResp struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tStatus       int    `json:\"status\"`\n\t\tErrno        int    `json:\"errno\"`\n\t\tErrorInfo    string `json:\"error_info\"`\n\t\tReqID        string `json:\"req_id\"`\n\t\tAccessToken  string `json:\"access_token\"`\n\t\tRefreshToken string `json:\"refresh_token\"`\n\t\tExpiresIn    int    `json:\"expires_in\"`\n\t\tScope        string `json:\"scope\"`\n\t} `json:\"data\"`\n}\ntype Files struct {\n\tFid          string `json:\"fid\"`\n\tParentFid    string `json:\"parent_fid\"`\n\tCategory     int    `json:\"category\"`\n\tFilename     string `json:\"filename\"`\n\tSize         int64  `json:\"size\"`\n\tFileType     string `json:\"file_type\"`\n\tSubItems     int    `json:\"sub_items,omitempty\"`\n\tIsdir        int    `json:\"isdir\"`\n\tDuration     int    `json:\"duration\"`\n\tCreatedAt    int64  `json:\"created_at\"`\n\tUpdatedAt    int64  `json:\"updated_at\"`\n\tIsBackup     int    `json:\"is_backup\"`\n\tThumbnailURL string `json:\"thumbnail_url,omitempty\"`\n}\n\nfunc (f *Files) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f *Files) GetName() string {\n\treturn f.Filename\n}\n\nfunc (f *Files) ModTime() time.Time {\n\t//return time.Unix(f.UpdatedAt, 0)\n\treturn time.Unix(0, f.UpdatedAt*int64(time.Millisecond))\n}\n\nfunc (f *Files) CreateTime() time.Time {\n\t//return time.Unix(f.CreatedAt, 0)\n\treturn time.Unix(0, f.CreatedAt*int64(time.Millisecond))\n}\n\nfunc (f *Files) IsDir() bool {\n\treturn f.Isdir == 1\n}\n\nfunc (f *Files) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *Files) GetID() string {\n\treturn f.Fid\n}\n\nfunc (f *Files) GetPath() string {\n\treturn \"\"\n}\n\nvar _ model.Obj = (*Files)(nil)\n\ntype FilesData struct {\n\tCommonRsp\n\tData struct {\n\t\tTotalCount int64   `json:\"total_count\"`\n\t\tFiles      []Files `json:\"files\"`\n\t} `json:\"data\"`\n}\n\ntype StreamingFileLink struct {\n\tCommonRsp\n\tData struct {\n\t\tDefaultResolution string `json:\"default_resolution\"`\n\t\tLastPlayTime      int    `json:\"last_play_time\"`\n\t\tVideoInfo         []struct {\n\t\t\tResolution  string  `json:\"resolution\"`\n\t\t\tAccessable  int     `json:\"accessable\"`\n\t\t\tTransStatus string  `json:\"trans_status\"`\n\t\t\tDuration    int     `json:\"duration,omitempty\"`\n\t\t\tSize        int64   `json:\"size,omitempty\"`\n\t\t\tFormat      string  `json:\"format,omitempty\"`\n\t\t\tWidth       int     `json:\"width,omitempty\"`\n\t\t\tHeight      int     `json:\"height,omitempty\"`\n\t\t\tURL         string  `json:\"url,omitempty\"`\n\t\t\tBitrate     float64 `json:\"bitrate,omitempty\"`\n\t\t\tDolbyVision struct {\n\t\t\t\tProfile int `json:\"profile\"`\n\t\t\t\tLevel   int `json:\"level\"`\n\t\t\t} `json:\"dolby_vision,omitempty\"`\n\t\t} `json:\"video_info\"`\n\t\tAudioInfo []interface{} `json:\"audio_info\"`\n\t} `json:\"data\"`\n}\n\ntype DownloadFileLink struct {\n\tCommonRsp\n\tData struct {\n\t\tFid         string `json:\"fid\"`\n\t\tFileName    string `json:\"file_name\"`\n\t\tSize        int64  `json:\"size\"`\n\t\tDownloadURL string `json:\"download_url\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/quark_uc_tv/util.go",
    "content": "package quark_uc_tv\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tUserAgent    = \"Mozilla/5.0 (Linux; U; Android 13; zh-cn; M2004J7AC Build/UKQ1.231108.001) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1\"\n\tDeviceBrand  = \"Xiaomi\"\n\tPlatform     = \"tv\"\n\tDeviceName   = \"M2004J7AC\"\n\tDeviceModel  = \"M2004J7AC\"\n\tBuildDevice  = \"M2004J7AC\"\n\tBuildProduct = \"M2004J7AC\"\n\tDeviceGpu    = \"Adreno (TM) 550\"\n\tActivityRect = \"{}\"\n)\n\nfunc (d *QuarkUCTV) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tu := d.conf.api + pathname\n\ttm, token, reqID := d.generateReqSign(method, pathname, d.conf.signKey)\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeaders(map[string]string{\n\t\t\"Accept\":          \"application/json, text/plain, */*\",\n\t\t\"User-Agent\":      UserAgent,\n\t\t\"x-pan-tm\":        tm,\n\t\t\"x-pan-token\":     token,\n\t\t\"x-pan-client-id\": d.conf.clientID,\n\t})\n\treq.SetQueryParams(map[string]string{\n\t\t\"req_id\":        reqID,\n\t\t\"access_token\":  d.QuarkUCTVCommon.AccessToken,\n\t\t\"app_ver\":       d.conf.appVer,\n\t\t\"device_id\":     d.Addition.DeviceID,\n\t\t\"device_brand\":  DeviceBrand,\n\t\t\"platform\":      Platform,\n\t\t\"device_name\":   DeviceName,\n\t\t\"device_model\":  DeviceModel,\n\t\t\"build_device\":  BuildDevice,\n\t\t\"build_product\": BuildProduct,\n\t\t\"device_gpu\":    DeviceGpu,\n\t\t\"activity_rect\": ActivityRect,\n\t\t\"channel\":       d.conf.channel,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e Resp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 判断 是否需要 刷新 access_token\n\terrInfoLower := strings.ToLower(strings.TrimSpace(e.ErrorInfo))\n\tmaybeTokenInvalid :=\n\t\t(e.Status == -1 && (e.Errno == 10001 || e.Errno == 11001)) ||\n\t\t\t(errInfoLower != \"\" &&\n\t\t\t\t(strings.Contains(errInfoLower, \"access token\") ||\n\t\t\t\t\tstrings.Contains(errInfoLower, \"access_token\") ||\n\t\t\t\t\tstrings.Contains(errInfoLower, \"token无效\") ||\n\t\t\t\t\tstrings.Contains(errInfoLower, \"token 无效\")))\n\tif maybeTokenInvalid {\n\t\t// token 过期 / 无效\n\t\terr = d.getRefreshTokenByTV(ctx, d.Addition.RefreshToken, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tctx1, cancelFunc := context.WithTimeout(ctx, 10*time.Second)\n\t\tdefer cancelFunc()\n\t\treturn d.request(ctx1, pathname, method, callback, resp)\n\t}\n\n\tif e.Status >= 400 || e.Errno != 0 {\n\t\treturn nil, errors.New(e.ErrorInfo)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *QuarkUCTV) getLoginCode(ctx context.Context) (string, error) {\n\t// 获取登录二维码\n\tpathname := \"/oauth/authorize\"\n\tvar resp struct {\n\t\tCommonRsp\n\t\tQrData     string `json:\"qr_data\"`\n\t\tQueryToken string `json:\"query_token\"`\n\t}\n\t_, err := d.request(ctx, pathname, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"auth_type\": \"code\",\n\t\t\t\"client_id\": d.conf.clientID,\n\t\t\t\"scope\":     \"netdisk\",\n\t\t\t\"qrcode\":    \"1\",\n\t\t\t\"qr_width\":  \"460\",\n\t\t\t\"qr_height\": \"460\",\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// 保存query_token 用于后续登录\n\tif resp.QueryToken != \"\" {\n\t\td.Addition.QueryToken = resp.QueryToken\n\t\top.MustSaveDriverStorage(d)\n\t}\n\treturn resp.QrData, nil\n}\n\nfunc (d *QuarkUCTV) getCode(ctx context.Context) (string, error) {\n\t// 通过query token获取code\n\tpathname := \"/oauth/code\"\n\tvar resp struct {\n\t\tCommonRsp\n\t\tCode string `json:\"code\"`\n\t}\n\t_, err := d.request(ctx, pathname, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"client_id\":   d.conf.clientID,\n\t\t\t\"scope\":       \"netdisk\",\n\t\t\t\"query_token\": d.Addition.QueryToken,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Code, nil\n}\n\nfunc (d *QuarkUCTV) getRefreshTokenByTV(ctx context.Context, code string, isRefresh bool) error {\n\tpathname := \"/token\"\n\t_, _, reqID := d.generateReqSign(http.MethodPost, pathname, d.conf.signKey)\n\tu := d.conf.codeApi + pathname\n\tvar resp RefreshTokenAuthResp\n\tbody := map[string]string{\n\t\t\"req_id\":        reqID,\n\t\t\"app_ver\":       d.conf.appVer,\n\t\t\"device_id\":     d.Addition.DeviceID,\n\t\t\"device_brand\":  DeviceBrand,\n\t\t\"platform\":      Platform,\n\t\t\"device_name\":   DeviceName,\n\t\t\"device_model\":  DeviceModel,\n\t\t\"build_device\":  BuildDevice,\n\t\t\"build_product\": BuildProduct,\n\t\t\"device_gpu\":    DeviceGpu,\n\t\t\"activity_rect\": ActivityRect,\n\t\t\"channel\":       d.conf.channel,\n\t}\n\tif isRefresh {\n\t\tbody[\"refresh_token\"] = code\n\t} else {\n\t\tbody[\"code\"] = code\n\t}\n\n\t_, err := base.RestyClient.R().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetBody(body).\n\t\tSetResult(&resp).\n\t\tSetContext(ctx).\n\t\tPost(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 200 {\n\t\treturn errors.New(resp.Message)\n\t}\n\tif resp.Data.RefreshToken != \"\" {\n\t\td.Addition.RefreshToken = resp.Data.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\td.QuarkUCTVCommon.AccessToken = resp.Data.AccessToken\n\t} else {\n\t\treturn errors.New(\"refresh token is empty\")\n\t}\n\treturn nil\n}\n\nfunc (d *QuarkUCTV) isLogin(ctx context.Context) (bool, error) {\n\t_, err := d.request(ctx, \"/user\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"method\": \"user_info\",\n\t\t})\n\t}, nil)\n\treturn err == nil, err\n}\n\nfunc (d *QuarkUCTV) generateReqSign(method string, pathname string, key string) (string, string, string) {\n\t//timestamp 13位时间戳\n\ttimestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)\n\tdeviceID := d.Addition.DeviceID\n\tif deviceID == \"\" {\n\t\tdeviceID = utils.GetMD5EncodeStr(timestamp)\n\t\td.Addition.DeviceID = deviceID\n\t\top.MustSaveDriverStorage(d)\n\t}\n\t// 生成req_id\n\treqID := md5.Sum([]byte(deviceID + timestamp))\n\treqIDHex := hex.EncodeToString(reqID[:])\n\n\t// 生成x_pan_token\n\ttokenData := method + \"&\" + pathname + \"&\" + timestamp + \"&\" + key\n\txPanToken := sha256.Sum256([]byte(tokenData))\n\txPanTokenHex := hex.EncodeToString(xPanToken[:])\n\n\treturn timestamp, xPanTokenHex, reqIDHex\n}\n\nfunc (d *QuarkUCTV) getTranscodingLink(ctx context.Context, file model.Obj) (*model.Link, error) {\n\tvar fileLink StreamingFileLink\n\t_, err := d.request(ctx, \"/file\", \"GET\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"method\":     \"streaming\",\n\t\t\t\"group_by\":   \"source\",\n\t\t\t\"fid\":        file.GetID(),\n\t\t\t\"resolution\": \"low,normal,high,super,2k,4k\",\n\t\t\t\"support\":    \"dolby_vision\",\n\t\t})\n\t}, &fileLink)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, info := range fileLink.Data.VideoInfo {\n\t\tif info.URL != \"\" {\n\t\t\treturn &model.Link{\n\t\t\t\tURL:           info.URL,\n\t\t\t\tContentLength: info.Size,\n\t\t\t\tConcurrency:   3,\n\t\t\t\tPartSize:      10 * utils.MB,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no link found\")\n}\n\nfunc (d *QuarkUCTV) getDownloadLink(ctx context.Context, file model.Obj) (*model.Link, error) {\n\tvar fileLink DownloadFileLink\n\t_, err := d.request(ctx, \"/file\", \"GET\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"method\":     \"download\",\n\t\t\t\"group_by\":   \"source\",\n\t\t\t\"fid\":        file.GetID(),\n\t\t\t\"resolution\": \"low,normal,high,super,2k,4k\",\n\t\t\t\"support\":    \"dolby_vision\",\n\t\t})\n\t}, &fileLink)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL:         fileLink.Data.DownloadURL,\n\t\tConcurrency: 3,\n\t\tPartSize:    10 * utils.MB,\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/s3/doge.go",
    "content": "package s3\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\ntype TmpTokenResponse struct {\n\tCode int                  `json:\"code\"`\n\tMsg  string               `json:\"msg\"`\n\tData TmpTokenResponseData `json:\"data,omitempty\"`\n}\ntype TmpTokenResponseData struct {\n\tCredentials Credentials `json:\"Credentials\"`\n\tExpiredAt   int         `json:\"ExpiredAt\"`\n}\ntype Credentials struct {\n\tAccessKeyId     string `json:\"accessKeyId,omitempty\"`\n\tSecretAccessKey string `json:\"secretAccessKey,omitempty\"`\n\tSessionToken    string `json:\"sessionToken,omitempty\"`\n}\n\nfunc getCredentials(AccessKey, SecretKey string) (rst Credentials, err error) {\n\tapiPath := \"/auth/tmp_token.json\"\n\treqBody, err := json.Marshal(map[string]interface{}{\"channel\": \"OSS_FULL\", \"scopes\": []string{\"*\"}})\n\tif err != nil {\n\t\treturn rst, err\n\t}\n\n\tsignStr := apiPath + \"\\n\" + string(reqBody)\n\thmacObj := hmac.New(sha1.New, []byte(SecretKey))\n\thmacObj.Write([]byte(signStr))\n\tsign := hex.EncodeToString(hmacObj.Sum(nil))\n\tAuthorization := \"TOKEN \" + AccessKey + \":\" + sign\n\n\treq, err := http.NewRequest(http.MethodPost, \"https://api.dogecloud.com\"+apiPath, strings.NewReader(string(reqBody)))\n\tif err != nil {\n\t\treturn rst, err\n\t}\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"Authorization\", Authorization)\n\tclient := http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn rst, err\n\t}\n\tdefer resp.Body.Close()\n\tret, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn rst, err\n\t}\n\tvar tmpTokenResp TmpTokenResponse\n\terr = json.Unmarshal(ret, &tmpTokenResp)\n\tif err != nil {\n\t\treturn rst, err\n\t}\n\treturn tmpTokenResp.Data.Credentials, nil\n}\n"
  },
  {
    "path": "drivers/s3/driver.go",
    "content": "package s3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype S3 struct {\n\tmodel.Storage\n\tAddition\n\tSession            *session.Session\n\tclient             *s3.S3\n\tlinkClient         *s3.S3\n\tdirectUploadClient *s3.S3\n\n\tconfig driver.Config\n\tcron   *cron.Cron\n}\n\nfunc (d *S3) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *S3) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *S3) Init(ctx context.Context) error {\n\tif d.Region == \"\" {\n\t\td.Region = \"openlist\"\n\t}\n\tif d.config.Name == \"Doge\" {\n\t\t// 多吉云每次临时生成的秘钥有效期为 2h，所以这里设置为 118 分钟重新生成一次\n\t\td.cron = cron.NewCron(time.Minute * 118)\n\t\td.cron.Do(func() {\n\t\t\terr := d.initSession()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorln(\"Doge init session error:\", err)\n\t\t\t}\n\t\t\td.client = d.getClient(ClientTypeNormal)\n\t\t\td.linkClient = d.getClient(ClientTypeLink)\n\t\t\td.directUploadClient = d.getClient(ClientTypeDirectUpload)\n\t\t})\n\t}\n\terr := d.initSession()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.client = d.getClient(ClientTypeNormal)\n\td.linkClient = d.getClient(ClientTypeLink)\n\td.directUploadClient = d.getClient(ClientTypeDirectUpload)\n\treturn nil\n}\n\nfunc (d *S3) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\treturn nil\n}\n\nfunc (d *S3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif d.ListObjectVersion == \"v2\" {\n\t\treturn d.listV2(dir.GetPath(), args)\n\t}\n\treturn d.listV1(dir.GetPath(), args)\n}\n\nfunc (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tpath := getKey(file.GetPath(), false)\n\tfileName := stdpath.Base(path)\n\tinput := &s3.GetObjectInput{\n\t\tBucket: &d.Bucket,\n\t\tKey:    &path,\n\t\t//ResponseContentDisposition: &disposition,\n\t}\n\n\tif d.CustomHost == \"\" {\n\t\tdisposition := fmt.Sprintf(`attachment; filename*=UTF-8''%s`, url.PathEscape(fileName))\n\t\tif d.AddFilenameToDisposition {\n\t\t\tdisposition = utils.GenerateContentDisposition(fileName)\n\t\t}\n\t\tinput.ResponseContentDisposition = &disposition\n\t}\n\n\treq, _ := d.linkClient.GetObjectRequest(input)\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"failed to create GetObject request\")\n\t}\n\tvar link model.Link\n\tvar err error\n\tif d.CustomHost != \"\" {\n\t\tif d.EnableCustomHostPresign {\n\t\t\tlink.URL, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))\n\t\t} else {\n\t\t\terr = req.Build()\n\t\t\tlink.URL = req.HTTPRequest.URL.String()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to generate link URL: %w\", err)\n\t\t}\n\n\t\tif d.RemoveBucket {\n\t\t\tparsedURL, parseErr := url.Parse(link.URL)\n\t\t\tif parseErr != nil {\n\t\t\t\tlog.Errorf(\"Failed to parse URL for bucket removal: %v, URL: %s\", parseErr, link.URL)\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse URL for bucket removal: %w\", parseErr)\n\t\t\t}\n\n\t\t\tpath := parsedURL.Path\n\t\t\tbucketPrefix := \"/\" + d.Bucket\n\t\t\tif strings.HasPrefix(path, bucketPrefix) {\n\t\t\t\tpath = strings.TrimPrefix(path, bucketPrefix)\n\t\t\t\tif path == \"\" {\n\t\t\t\t\tpath = \"/\"\n\t\t\t\t}\n\t\t\t\tparsedURL.Path = path\n\t\t\t\tlink.URL = parsedURL.String()\n\t\t\t\tlog.Debugf(\"Removed bucket '%s' from URL path: %s -> %s\", d.Bucket, bucketPrefix, path)\n\t\t\t} else {\n\t\t\t\tlog.Warnf(\"URL path does not contain expected bucket prefix '%s': %s\", bucketPrefix, path)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif common.ShouldProxy(d, fileName) {\n\t\t\terr = req.Sign()\n\t\t\tlink.URL = req.HTTPRequest.URL.String()\n\t\t\tlink.Header = req.HTTPRequest.Header\n\t\t} else {\n\t\t\tlink.URL, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &link, nil\n}\n\nfunc (d *S3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.Put(ctx, &model.Object{\n\t\tPath: stdpath.Join(parentDir.GetPath(), dirName),\n\t}, &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tName:     getPlaceholderName(d.Placeholder),\n\t\t\tModified: time.Now(),\n\t\t},\n\t\tReader:   bytes.NewReader([]byte{}),\n\t\tMimetype: \"application/octet-stream\",\n\t}, func(float64) {})\n}\n\nfunc (d *S3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\terr := d.Copy(ctx, srcObj, dstDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.Remove(ctx, srcObj)\n}\n\nfunc (d *S3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\terr := d.copy(ctx, srcObj.GetPath(), stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName), srcObj.IsDir())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.Remove(ctx, srcObj)\n}\n\nfunc (d *S3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.copy(ctx, srcObj.GetPath(), stdpath.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.IsDir())\n}\n\nfunc (d *S3) Remove(ctx context.Context, obj model.Obj) error {\n\tif obj.IsDir() {\n\t\treturn d.removeDir(ctx, obj.GetPath())\n\t}\n\treturn d.removeFile(obj.GetPath())\n}\n\nfunc (d *S3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tuploader := s3manager.NewUploader(d.Session)\n\tif s.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\tuploader.PartSize = s.GetSize() / (s3manager.MaxUploadParts - 1)\n\t}\n\tkey := getKey(stdpath.Join(dstDir.GetPath(), s.GetName()), false)\n\tcontentType := s.GetMimetype()\n\tlog.Debugln(\"key:\", key)\n\tinput := &s3manager.UploadInput{\n\t\tBucket: &d.Bucket,\n\t\tKey:    &key,\n\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader:         s,\n\t\t\tUpdateProgress: up,\n\t\t}),\n\t\tContentType: &contentType,\n\t}\n\t_, err := uploader.UploadWithContext(ctx, input)\n\treturn err\n}\n\nfunc (d *S3) GetDirectUploadTools() []string {\n\tif !d.EnableDirectUpload {\n\t\treturn nil\n\t}\n\treturn []string{\"HttpDirect\"}\n}\n\nfunc (d *S3) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {\n\tif !d.EnableDirectUpload {\n\t\treturn nil, errs.NotImplement\n\t}\n\tpath := getKey(stdpath.Join(dstDir.GetPath(), fileName), false)\n\treq, _ := d.directUploadClient.PutObjectRequest(&s3.PutObjectInput{\n\t\tBucket: &d.Bucket,\n\t\tKey:    &path,\n\t})\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"failed to create PutObject request\")\n\t}\n\tlink, err := req.Presign(time.Hour * time.Duration(d.SignURLExpire))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.HttpDirectUploadInfo{\n\t\tUploadURL: link,\n\t\tMethod:    \"PUT\",\n\t}, nil\n}\n\nvar _ driver.Driver = (*S3)(nil)\n"
  },
  {
    "path": "drivers/s3/meta.go",
    "content": "package s3\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tBucket                   string `json:\"bucket\" required:\"true\"`\n\tEndpoint                 string `json:\"endpoint\" required:\"true\"`\n\tRegion                   string `json:\"region\"`\n\tAccessKeyID              string `json:\"access_key_id\" required:\"true\"`\n\tSecretAccessKey          string `json:\"secret_access_key\" required:\"true\"`\n\tSessionToken             string `json:\"session_token\"`\n\tCustomHost               string `json:\"custom_host\"`\n\tEnableCustomHostPresign  bool   `json:\"enable_custom_host_presign\"`\n\tSignURLExpire            int    `json:\"sign_url_expire\" type:\"number\" default:\"4\"`\n\tPlaceholder              string `json:\"placeholder\"`\n\tForcePathStyle           bool   `json:\"force_path_style\"`\n\tListObjectVersion        string `json:\"list_object_version\" type:\"select\" options:\"v1,v2\" default:\"v1\"`\n\tRemoveBucket             bool   `json:\"remove_bucket\" help:\"Remove bucket name from path when using custom host.\"`\n\tAddFilenameToDisposition bool   `json:\"add_filename_to_disposition\" help:\"Add filename to Content-Disposition header.\"`\n\tEnableDirectUpload       bool   `json:\"enable_direct_upload\" default:\"false\"`\n\tDirectUploadHost         string `json:\"direct_upload_host\" required:\"false\"`\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &S3{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:        \"S3\",\n\t\t\t\tDefaultRoot: \"/\",\n\t\t\t\tLocalSort:   true,\n\t\t\t\tCheckStatus: true,\n\t\t\t},\n\t\t}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &S3{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:        \"Doge\",\n\t\t\t\tDefaultRoot: \"/\",\n\t\t\t\tLocalSort:   true,\n\t\t\t\tCheckStatus: true,\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/s3/types.go",
    "content": "package s3\n"
  },
  {
    "path": "drivers/s3/util.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/request\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *S3) initSession() error {\n\tvar err error\n\taccessKeyID, secretAccessKey, sessionToken := d.AccessKeyID, d.SecretAccessKey, d.SessionToken\n\tif d.config.Name == \"Doge\" {\n\t\tcredentialsTmp, err := getCredentials(d.AccessKeyID, d.SecretAccessKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\taccessKeyID, secretAccessKey, sessionToken = credentialsTmp.AccessKeyId, credentialsTmp.SecretAccessKey, credentialsTmp.SessionToken\n\t}\n\tcfg := &aws.Config{\n\t\tCredentials:      credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken),\n\t\tRegion:           &d.Region,\n\t\tEndpoint:         &d.Endpoint,\n\t\tS3ForcePathStyle: aws.Bool(d.ForcePathStyle),\n\t}\n\td.Session, err = session.NewSession(cfg)\n\treturn err\n}\n\nconst (\n\tClientTypeNormal = iota\n\tClientTypeLink\n\tClientTypeDirectUpload\n)\n\nfunc (d *S3) getClient(clientType int) *s3.S3 {\n\tclient := s3.New(d.Session)\n\tif clientType == ClientTypeLink && d.CustomHost != \"\" {\n\t\tclient.Handlers.Build.PushBack(func(r *request.Request) {\n\t\t\tif r.HTTPRequest.Method != http.MethodGet {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t//判断CustomHost是否以http://或https://开头\n\t\t\tsplit := strings.SplitN(d.CustomHost, \"://\", 2)\n\t\t\tif utils.SliceContains([]string{\"http\", \"https\"}, split[0]) {\n\t\t\t\tr.HTTPRequest.URL.Scheme = split[0]\n\t\t\t\tr.HTTPRequest.URL.Host = split[1]\n\t\t\t} else {\n\t\t\t\tr.HTTPRequest.URL.Host = d.CustomHost\n\t\t\t}\n\t\t})\n\t}\n\tif clientType == ClientTypeDirectUpload && d.DirectUploadHost != \"\" {\n\t\tclient.Handlers.Build.PushBack(func(r *request.Request) {\n\t\t\tif r.HTTPRequest.Method != http.MethodPut {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsplit := strings.SplitN(d.DirectUploadHost, \"://\", 2)\n\t\t\tif utils.SliceContains([]string{\"http\", \"https\"}, split[0]) {\n\t\t\t\tr.HTTPRequest.URL.Scheme = split[0]\n\t\t\t\tr.HTTPRequest.URL.Host = split[1]\n\t\t\t} else {\n\t\t\t\tr.HTTPRequest.URL.Host = d.DirectUploadHost\n\t\t\t}\n\t\t})\n\t}\n\treturn client\n}\n\nfunc getKey(path string, dir bool) string {\n\tpath = strings.TrimPrefix(path, \"/\")\n\tif path != \"\" && dir {\n\t\tpath += \"/\"\n\t}\n\treturn path\n}\n\nvar defaultPlaceholderName = \".openlist\"\n\nfunc getPlaceholderName(placeholder string) string {\n\tif placeholder == \"\" {\n\t\treturn defaultPlaceholderName\n\t}\n\treturn placeholder\n}\n\nfunc (d *S3) listV1(dirPath string, args model.ListArgs) ([]model.Obj, error) {\n\tprefix := getKey(dirPath, true)\n\tlog.Debugf(\"list: %s\", prefix)\n\tfiles := make([]model.Obj, 0)\n\tmarker := \"\"\n\tfor {\n\t\tinput := &s3.ListObjectsInput{\n\t\t\tBucket:    &d.Bucket,\n\t\t\tMarker:    &marker,\n\t\t\tPrefix:    &prefix,\n\t\t\tDelimiter: aws.String(\"/\"),\n\t\t}\n\t\tlistObjectsResult, err := d.client.ListObjects(input)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, object := range listObjectsResult.CommonPrefixes {\n\t\t\tname := path.Base(strings.Trim(*object.Prefix, \"/\"))\n\t\t\tfile := model.Object{\n\t\t\t\tPath:     path.Join(dirPath, name),\n\t\t\t\tName:     name,\n\t\t\t\tModified: d.Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t}\n\t\t\tfiles = append(files, &file)\n\t\t}\n\t\tfor _, object := range listObjectsResult.Contents {\n\t\t\tname := path.Base(*object.Key)\n\t\t\tif !args.S3ShowPlaceholder && (name == getPlaceholderName(d.Placeholder) || name == d.Placeholder) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfile := model.Object{\n\t\t\t\tPath:     path.Join(dirPath, name),\n\t\t\t\tName:     name,\n\t\t\t\tSize:     *object.Size,\n\t\t\t\tModified: *object.LastModified,\n\t\t\t}\n\t\t\tfiles = append(files, &file)\n\t\t}\n\t\tif listObjectsResult.IsTruncated == nil {\n\t\t\treturn nil, errors.New(\"IsTruncated nil\")\n\t\t}\n\t\tif *listObjectsResult.IsTruncated {\n\t\t\tmarker = *listObjectsResult.NextMarker\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (d *S3) listV2(dirPath string, args model.ListArgs) ([]model.Obj, error) {\n\tprefix := getKey(dirPath, true)\n\tfiles := make([]model.Obj, 0)\n\tvar continuationToken, startAfter *string\n\tfor {\n\t\tinput := &s3.ListObjectsV2Input{\n\t\t\tBucket:            &d.Bucket,\n\t\t\tContinuationToken: continuationToken,\n\t\t\tPrefix:            &prefix,\n\t\t\tDelimiter:         aws.String(\"/\"),\n\t\t\tStartAfter:        startAfter,\n\t\t}\n\t\tlistObjectsResult, err := d.client.ListObjectsV2(input)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debugf(\"resp: %+v\", listObjectsResult)\n\t\tfor _, object := range listObjectsResult.CommonPrefixes {\n\t\t\tname := path.Base(strings.Trim(*object.Prefix, \"/\"))\n\t\t\tfile := model.Object{\n\t\t\t\tPath:     path.Join(dirPath, name),\n\t\t\t\tName:     name,\n\t\t\t\tModified: d.Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t}\n\t\t\tfiles = append(files, &file)\n\t\t}\n\t\tfor _, object := range listObjectsResult.Contents {\n\t\t\tif strings.HasSuffix(*object.Key, \"/\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := path.Base(*object.Key)\n\t\t\tif !args.S3ShowPlaceholder && (name == getPlaceholderName(d.Placeholder) || name == d.Placeholder) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfile := model.Object{\n\t\t\t\tPath:     path.Join(dirPath, name),\n\t\t\t\tName:     name,\n\t\t\t\tSize:     *object.Size,\n\t\t\t\tModified: *object.LastModified,\n\t\t\t}\n\t\t\tfiles = append(files, &file)\n\t\t}\n\t\tif !aws.BoolValue(listObjectsResult.IsTruncated) {\n\t\t\tbreak\n\t\t}\n\t\tif listObjectsResult.NextContinuationToken != nil {\n\t\t\tcontinuationToken = listObjectsResult.NextContinuationToken\n\t\t\tcontinue\n\t\t}\n\t\tif len(listObjectsResult.Contents) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tstartAfter = listObjectsResult.Contents[len(listObjectsResult.Contents)-1].Key\n\t}\n\treturn files, nil\n}\n\nfunc (d *S3) copy(ctx context.Context, src string, dst string, isDir bool) error {\n\tif isDir {\n\t\treturn d.copyDir(ctx, src, dst)\n\t}\n\treturn d.copyFile(ctx, src, dst)\n}\n\nfunc (d *S3) copyFile(ctx context.Context, src string, dst string) error {\n\tsrcKey := getKey(src, false)\n\tdstKey := getKey(dst, false)\n\tencodedKey := strings.ReplaceAll(url.PathEscape(d.Bucket+\"/\"+srcKey), \"+\", \"%2B\")\n\tinput := &s3.CopyObjectInput{\n\t\tBucket:     &d.Bucket,\n\t\tCopySource: aws.String(encodedKey),\n\t\tKey:        &dstKey,\n\t}\n\t_, err := d.client.CopyObject(input)\n\treturn err\n}\n\nfunc (d *S3) copyDir(ctx context.Context, src string, dst string) error {\n\tobjs, err := op.List(ctx, d, src, model.ListArgs{S3ShowPlaceholder: true})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, obj := range objs {\n\t\tcSrc := path.Join(src, obj.GetName())\n\t\tcDst := path.Join(dst, obj.GetName())\n\t\tif obj.IsDir() {\n\t\t\terr = d.copyDir(ctx, cSrc, cDst)\n\t\t} else {\n\t\t\terr = d.copyFile(ctx, cSrc, cDst)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *S3) removeDir(ctx context.Context, src string) error {\n\tobjs, err := op.List(ctx, d, src, model.ListArgs{})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, obj := range objs {\n\t\tcSrc := path.Join(src, obj.GetName())\n\t\tif obj.IsDir() {\n\t\t\terr = d.removeDir(ctx, cSrc)\n\t\t} else {\n\t\t\terr = d.removeFile(cSrc)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_ = d.removeFile(path.Join(src, getPlaceholderName(d.Placeholder)))\n\t_ = d.removeFile(path.Join(src, d.Placeholder))\n\treturn nil\n}\n\nfunc (d *S3) removeFile(src string) error {\n\tkey := getKey(src, false)\n\tinput := &s3.DeleteObjectInput{\n\t\tBucket: &d.Bucket,\n\t\tKey:    &key,\n\t}\n\t_, err := d.client.DeleteObject(input)\n\treturn err\n}\n"
  },
  {
    "path": "drivers/seafile/driver.go",
    "content": "package seafile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Seafile struct {\n\tmodel.Storage\n\tAddition\n\n\tauthorization string\n\troot          model.Obj\n}\n\nfunc (d *Seafile) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Seafile) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Seafile) Init(ctx context.Context) error {\n\td.Address = strings.TrimSuffix(d.Address, \"/\")\n\terr := d.getToken()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)\n\tif d.RepoId != \"\" {\n\t\tlibrary, err := d.getLibraryInfo(d.RepoId)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlibrary.path = d.RootFolderPath\n\t\tlibrary.ObjMask = model.Locked\n\t\td.root = &LibraryInfo{\n\t\t\tLibraryItemResp: library,\n\t\t}\n\t\treturn nil\n\t}\n\tif len(d.RootFolderPath) <= 1 {\n\t\td.root = &model.Object{\n\t\t\tName:     \"root\",\n\t\t\tPath:     d.RootFolderPath,\n\t\t\tIsFolder: true,\n\t\t\tModified: d.Modified,\n\t\t\tMask:     model.Locked,\n\t\t}\n\t\treturn nil\n\t}\n\n\tvar resp []LibraryItemResp\n\t_, err = d.request(http.MethodGet, \"/api2/repos/\", func(req *resty.Request) {\n\t\treq.SetResult(&resp)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, library := range resp {\n\t\tp, found := strings.CutPrefix(d.RootFolderPath[1:], library.Name)\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\t\tif p == \"\" {\n\t\t\tp = \"/\"\n\t\t} else if p[0] != '/' {\n\t\t\tcontinue\n\t\t}\n\t\t// d.RepoId = library.Id\n\t\t// d.RootFolderPath = p\n\n\t\tlibrary.path = p\n\t\tlibrary.ObjMask = model.Locked\n\t\td.root = &LibraryInfo{\n\t\t\tLibraryItemResp: library,\n\t\t}\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"Library for root folder path %q not found\", d.RootFolderPath)\n}\n\nfunc (d *Seafile) Drop(ctx context.Context) error {\n\td.root = nil\n\treturn nil\n}\n\nfunc (d *Seafile) GetRoot(ctx context.Context) (model.Obj, error) {\n\tif d.root == nil {\n\t\treturn nil, errs.StorageNotInit\n\t}\n\treturn d.root, nil\n}\n\nfunc (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {\n\tpath := dir.GetPath()\n\tswitch o := dir.(type) {\n\tdefault:\n\t\tvar resp []LibraryItemResp\n\t\t_, err = d.request(http.MethodGet, \"/api2/repos/\", func(req *resty.Request) {\n\t\t\treq.SetResult(&resp)\n\t\t})\n\t\treturn utils.SliceConvert(resp, func(f LibraryItemResp) (model.Obj, error) {\n\t\t\tf.path = path\n\t\t\treturn &LibraryInfo{\n\t\t\t\tLibraryItemResp: f,\n\t\t\t}, nil\n\t\t})\n\tcase *LibraryInfo:\n\t\tif o.Encrypted {\n\t\t\terr = d.decryptLibrary(o)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\tcase *RepoItemResp:\n\t\t// do nothing\n\t}\n\n\tvar resp []RepoItemResp\n\t_, err = d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/dir/\", dir.GetID()), func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(resp, func(f RepoItemResp) (model.Obj, error) {\n\t\tf.path = stdpath.Join(path, f.Name)\n\t\tf.repoID = dir.GetID()\n\t\treturn &f, nil\n\t})\n}\n\nfunc (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tres, err := d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/file/\", file.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\":     file.GetPath(),\n\t\t\t\"reuse\": \"1\",\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu := string(res)\n\tu = u[1 : len(u)-1] // remove quotes\n\treturn &model.Link{URL: u}, nil\n}\n\nfunc (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/dir/\", parentDir.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": stdpath.Join(parentDir.GetPath(), dirName),\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"mkdir\",\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", srcObj.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": srcObj.GetPath(),\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"move\",\n\t\t\t\"dst_repo\":  dstDir.GetID(),\n\t\t\t\"dst_dir\":   dstDir.GetPath(),\n\t\t})\n\t}, true)\n\treturn err\n}\n\nfunc (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err := d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", srcObj.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": srcObj.GetPath(),\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"rename\",\n\t\t\t\"newname\":   newName,\n\t\t})\n\t}, true)\n\treturn err\n}\n\nfunc (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", srcObj.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": srcObj.GetPath(),\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"copy\",\n\t\t\t\"dst_repo\":  dstDir.GetID(),\n\t\t\t\"dst_dir\":   dstDir.GetPath(),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := d.request(http.MethodDelete, fmt.Sprintf(\"/api2/repos/%s/file/\", obj.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": obj.GetPath(),\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Seafile) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tres, err := d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/upload-link/\", dstDir.GetID()), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": dstDir.GetPath(),\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu := string(res)\n\tu = u[1 : len(u)-1] // remove quotes\n\t_, err = d.request(http.MethodPost, u, func(req *resty.Request) {\n\t\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader:         s,\n\t\t\tUpdateProgress: up,\n\t\t})\n\t\treq.SetFileReader(\"file\", s.GetName(), r).\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"parent_dir\": dstDir.GetPath(),\n\t\t\t\t\"replace\":    \"1\",\n\t\t\t}).\n\t\t\tSetContext(ctx)\n\t})\n\treturn err\n}\n\nvar _ driver.Driver = (*Seafile)(nil)\n"
  },
  {
    "path": "drivers/seafile/meta.go",
    "content": "package seafile\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\n\tAddress  string `json:\"address\" required:\"true\"`\n\tUserName string `json:\"username\" required:\"false\"`\n\tPassword string `json:\"password\" required:\"false\"`\n\tToken    string `json:\"token\" required:\"false\"`\n\tRepoId   string `json:\"repoId\" required:\"false\"`\n\tRepoPwd  string `json:\"repoPwd\" required:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Seafile\",\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Seafile{}\n\t})\n}\n"
  },
  {
    "path": "drivers/seafile/types.go",
    "content": "package seafile\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype AuthTokenResp struct {\n\tToken string `json:\"token\"`\n}\n\ntype RepoItemResp struct {\n\tId         string `json:\"id\"`\n\tType       string `json:\"type\"` // repo, dir, file\n\tName       string `json:\"name\"`\n\tSize       int64  `json:\"size\"`\n\tModified   int64  `json:\"mtime\"`\n\tPermission string `json:\"permission\"`\n\n\tpath string\n\tmodel.ObjMask\n\trepoID string\n}\n\nfunc (l *RepoItemResp) IsDir() bool {\n\treturn l.Type == \"dir\"\n}\nfunc (l *RepoItemResp) GetPath() string {\n\treturn l.path\n}\nfunc (l *RepoItemResp) GetName() string {\n\treturn l.Name\n}\nfunc (l *RepoItemResp) ModTime() time.Time {\n\treturn time.Unix(l.Modified, 0)\n}\nfunc (l *RepoItemResp) CreateTime() time.Time {\n\treturn l.ModTime()\n}\nfunc (l *RepoItemResp) GetSize() int64 {\n\treturn l.Size\n}\nfunc (l *RepoItemResp) GetID() string {\n\tif l.repoID != \"\" {\n\t\treturn l.repoID\n\t}\n\treturn l.Id\n}\nfunc (l *RepoItemResp) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nvar _ model.Obj = (*RepoItemResp)(nil)\n\ntype LibraryItemResp struct {\n\tRepoItemResp\n\tOwnerContactEmail    string `json:\"owner_contact_email\"`\n\tOwnerName            string `json:\"owner_name\"`\n\tOwner                string `json:\"owner\"`\n\tModifierEmail        string `json:\"modifier_email\"`\n\tModifierContactEmail string `json:\"modifier_contact_email\"`\n\tModifierName         string `json:\"modifier_name\"`\n\tVirtual              bool   `json:\"virtual\"`\n\tMtimeRelative        string `json:\"mtime_relative\"`\n\tEncrypted            bool   `json:\"encrypted\"`\n\tVersion              int    `json:\"version\"`\n\tHeadCommitId         string `json:\"head_commit_id\"`\n\tRoot                 string `json:\"root\"`\n\tSalt                 string `json:\"salt\"`\n\tSizeFormatted        string `json:\"size_formatted\"`\n}\n\ntype LibraryInfo struct {\n\tLibraryItemResp\n\tdecryptedTime    time.Time\n\tdecryptedSuccess bool\n}\n\nfunc (l *LibraryInfo) IsDir() bool {\n\treturn true\n}\n"
  },
  {
    "path": "drivers/seafile/util.go",
    "content": "package seafile\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *Seafile) getToken() error {\n\tif d.Token != \"\" {\n\t\td.authorization = fmt.Sprintf(\"Token %s\", d.Token)\n\t\treturn nil\n\t}\n\tvar authResp AuthTokenResp\n\tres, err := base.RestyClient.R().\n\t\tSetResult(&authResp).\n\t\tSetFormData(map[string]string{\n\t\t\t\"username\": d.UserName,\n\t\t\t\"password\": d.Password,\n\t\t}).\n\t\tPost(d.Address + \"/api2/auth-token/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() >= 400 {\n\t\treturn fmt.Errorf(\"get token failed: %s\", res.String())\n\t}\n\td.authorization = fmt.Sprintf(\"Token %s\", authResp.Token)\n\treturn nil\n}\n\nfunc (d *Seafile) request(method string, pathname string, callback base.ReqCallback, noRedirect ...bool) ([]byte, error) {\n\tfull := pathname\n\tif !strings.HasPrefix(pathname, \"http\") {\n\t\tfull = d.Address + pathname\n\t}\n\treq := base.RestyClient.R()\n\tif len(noRedirect) > 0 && noRedirect[0] {\n\t\treq = base.NoRedirectClient.R()\n\t}\n\treq.SetHeader(\"Authorization\", d.authorization)\n\tcallback(req)\n\tvar (\n\t\tres *resty.Response\n\t\terr error\n\t)\n\tfor i := 0; i < 2; i++ {\n\t\tres, err = req.Execute(method, full)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif res.StatusCode() != 401 { // Unauthorized\n\t\t\tbreak\n\t\t}\n\t\terr = d.getToken()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif res.StatusCode() >= 400 {\n\t\treturn nil, fmt.Errorf(\"request failed: %s\", res.String())\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Seafile) getLibraryInfo(repoId string) (LibraryItemResp, error) {\n\tvar oneResp LibraryItemResp\n\t_, err := d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/\", repoId), func(req *resty.Request) {\n\t\treq.SetResult(&oneResp)\n\t})\n\treturn oneResp, err\n}\n\nvar repoPwdNotConfigured = errors.New(\"library password not configured\")\nvar repoPwdIncorrect = errors.New(\"library password is incorrect\")\n\nfunc (d *Seafile) decryptLibrary(repo *LibraryInfo) (err error) {\n\tif !repo.Encrypted {\n\t\treturn nil\n\t}\n\tif d.RepoPwd == \"\" {\n\t\treturn repoPwdNotConfigured\n\t}\n\tnow := time.Now()\n\tdecryptedTime := repo.decryptedTime\n\tif repo.decryptedSuccess {\n\t\tif now.Sub(decryptedTime).Minutes() <= 30 {\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\tif now.Sub(decryptedTime).Seconds() <= 10 {\n\t\t\treturn repoPwdIncorrect\n\t\t}\n\t}\n\tvar resp string\n\t_, err = d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetResult(&resp).SetFormData(map[string]string{\n\t\t\t\"password\": d.RepoPwd,\n\t\t})\n\t})\n\trepo.decryptedTime = time.Now()\n\tif err != nil || !strings.Contains(resp, \"success\") {\n\t\trepo.decryptedSuccess = false\n\t\treturn err\n\t}\n\trepo.decryptedSuccess = true\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/sftp/driver.go",
    "content": "package sftp\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/sftp\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype SFTP struct {\n\tmodel.Storage\n\tAddition\n\tclient                *sftp.Client\n\tclientConnectionError error\n}\n\nfunc (d *SFTP) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *SFTP) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *SFTP) Init(ctx context.Context) error {\n\treturn d._initClient()\n}\n\nfunc (d *SFTP) Drop(ctx context.Context) error {\n\tif d.client != nil {\n\t\t_ = d.client.Close()\n\t}\n\treturn nil\n}\n\nfunc (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"[sftp] list dir: %s\", dir.GetPath())\n\tfiles, err := d.client.ReadDir(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobjs, err := utils.SliceConvert(files, func(src os.FileInfo) (model.Obj, error) {\n\t\treturn d.fileToObj(src, dir.GetPath())\n\t})\n\treturn objs, err\n}\n\nfunc (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn nil, err\n\t}\n\tremoteFile, err := d.client.Open(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmFile := &stream.RateLimitFile{\n\t\tFile:    remoteFile,\n\t\tLimiter: stream.ServerDownloadLimit,\n\t\tCtx:     ctx,\n\t}\n\treturn &model.Link{\n\t\tRangeReader:      stream.GetRangeReaderFromMFile(file.GetSize(), mFile),\n\t\tSyncClosers:      utils.NewSyncClosers(remoteFile),\n\t\tRequireReference: true,\n\t}, nil\n}\n\nfunc (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn err\n\t}\n\treturn d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))\n}\n\nfunc (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn err\n\t}\n\treturn d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))\n}\n\nfunc (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn err\n\t}\n\treturn d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))\n}\n\nfunc (d *SFTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn err\n\t}\n\treturn d.remove(obj.GetPath())\n}\n\nfunc (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif err := d.clientReconnectOnConnectionError(); err != nil {\n\t\treturn err\n\t}\n\tdstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = dstFile.Close()\n\t}()\n\terr = utils.CopyWithCtx(ctx, dstFile, driver.NewLimitedUploadStream(ctx, stream), stream.GetSize(), up)\n\treturn err\n}\n\nfunc (d *SFTP) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tstat, err := d.client.StatVFS(d.RootFolderPath)\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"unimplemented\") {\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\t\treturn nil, err\n\t}\n\ttotal := int64(stat.Blocks * stat.Bsize)\n\tfree := int64(stat.Bfree * stat.Bsize)\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  total - free,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*SFTP)(nil)\n"
  },
  {
    "path": "drivers/sftp/meta.go",
    "content": "package sftp\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tAddress    string `json:\"address\" required:\"true\"`\n\tUsername   string `json:\"username\" required:\"true\"`\n\tPrivateKey string `json:\"private_key\" type:\"text\"`\n\tPassword   string `json:\"password\"`\n\tPassphrase string `json:\"passphrase\"`\n\tdriver.RootPath\n\tIgnoreSymlinkError bool `json:\"ignore_symlink_error\" default:\"false\" info:\"Ignore symlink error\"`\n}\n\nvar config = driver.Config{\n\tName:        \"SFTP\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tDefaultRoot: \"/\",\n\tCheckStatus: true,\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &SFTP{}\n\t})\n}\n"
  },
  {
    "path": "drivers/sftp/types.go",
    "content": "package sftp\n\nimport (\n\t\"os\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *SFTP) fileToObj(f os.FileInfo, dir string) (model.Obj, error) {\n\tsymlink := f.Mode()&os.ModeSymlink != 0\n\tpath := stdpath.Join(dir, f.Name())\n\tif !symlink {\n\t\treturn &model.Object{\n\t\t\tPath:     path,\n\t\t\tName:     f.Name(),\n\t\t\tSize:     f.Size(),\n\t\t\tModified: f.ModTime(),\n\t\t\tIsFolder: f.IsDir(),\n\t\t}, nil\n\t}\n\t// set target path\n\ttarget, err := d.client.ReadLink(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !strings.HasPrefix(target, \"/\") {\n\t\ttarget = stdpath.Join(dir, target)\n\t}\n\t_f, err := d.client.Stat(target)\n\tif err != nil {\n\t\tif d.IgnoreSymlinkError {\n\t\t\treturn &model.Object{\n\t\t\t\tPath:     path,\n\t\t\t\tName:     f.Name(),\n\t\t\t\tSize:     f.Size(),\n\t\t\t\tModified: f.ModTime(),\n\t\t\t\tIsFolder: f.IsDir(),\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\t// set basic info\n\tobj := &model.Object{\n\t\tName:     f.Name(),\n\t\tSize:     _f.Size(),\n\t\tModified: _f.ModTime(),\n\t\tIsFolder: _f.IsDir(),\n\t\tPath:     target,\n\t}\n\tlog.Debugf(\"[sftp] obj: %+v, is symlink: %v\", obj, symlink)\n\treturn obj, nil\n}\n"
  },
  {
    "path": "drivers/sftp/util.go",
    "content": "package sftp\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/pkg/sftp\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *SFTP) initClient() error {\n\t_, err, _ := singleflight.AnyGroup.Do(fmt.Sprintf(\"SFTP.initClient:%p\", d), func() (any, error) {\n\t\treturn nil, d._initClient()\n\t})\n\treturn err\n}\nfunc (d *SFTP) _initClient() error {\n\tvar auth ssh.AuthMethod\n\tif len(d.PrivateKey) > 0 {\n\t\tvar err error\n\t\tvar signer ssh.Signer\n\t\tif len(d.Passphrase) > 0 {\n\t\t\tsigner, err = ssh.ParsePrivateKeyWithPassphrase([]byte(d.PrivateKey), []byte(d.Passphrase))\n\t\t} else {\n\t\t\tsigner, err = ssh.ParsePrivateKey([]byte(d.PrivateKey))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tauth = ssh.PublicKeys(signer)\n\t} else {\n\t\tauth = ssh.Password(d.Password)\n\t}\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            d.Username,\n\t\tAuth:            []ssh.AuthMethod{auth},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t}\n\tconn, err := ssh.Dial(\"tcp\", d.Address, config)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.client, err = sftp.NewClient(conn)\n\tif err == nil {\n\t\td.clientConnectionError = nil\n\t\tgo func(d *SFTP) {\n\t\t\td.clientConnectionError = d.client.Wait()\n\t\t}(d)\n\t}\n\treturn err\n}\n\nfunc (d *SFTP) clientReconnectOnConnectionError() error {\n\terr := d.clientConnectionError\n\tif err == nil {\n\t\treturn nil\n\t}\n\tlog.Debugf(\"[sftp] discarding closed sftp connection: %v\", err)\n\tif d.client != nil {\n\t\t_ = d.client.Close()\n\t}\n\terr = d.initClient()\n\treturn err\n}\n\nfunc (d *SFTP) remove(remotePath string) error {\n\tf, err := d.client.Stat(remotePath)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tif f.IsDir() {\n\t\treturn d.removeDirectory(remotePath)\n\t} else {\n\t\treturn d.removeFile(remotePath)\n\t}\n}\n\nfunc (d *SFTP) removeDirectory(remotePath string) error {\n\tremoteFiles, err := d.client.ReadDir(remotePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, backupDir := range remoteFiles {\n\t\tremoteFilePath := path.Join(remotePath, backupDir.Name())\n\t\tif backupDir.IsDir() {\n\t\t\terr := d.removeDirectory(remoteFilePath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr := d.removeFile(remoteFilePath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn d.client.RemoveDirectory(remotePath)\n}\n\nfunc (d *SFTP) removeFile(remotePath string) error {\n\treturn d.client.Remove(path.Join(remotePath))\n}\n"
  },
  {
    "path": "drivers/smb/driver.go",
    "content": "package smb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/cloudsoda/go-smb2\"\n)\n\ntype SMB struct {\n\tlastConnTime int64\n\tmodel.Storage\n\tAddition\n\tfs *smb2.Share\n}\n\nfunc (d *SMB) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *SMB) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *SMB) Init(ctx context.Context) error {\n\tif !strings.Contains(d.Addition.Address, \":\") {\n\t\td.Addition.Address = d.Addition.Address + \":445\"\n\t}\n\treturn d._initFS(ctx)\n}\n\nfunc (d *SMB) Drop(ctx context.Context) error {\n\tif d.fs != nil {\n\t\t_ = d.fs.Umount()\n\t}\n\treturn nil\n}\n\nfunc (d *SMB) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tfullPath := dir.GetPath()\n\trawFiles, err := d.fs.ReadDir(fullPath)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn nil, err\n\t}\n\td.updateLastConnTime()\n\tfiles := make([]model.Obj, 0, len(rawFiles))\n\tfor _, f := range rawFiles {\n\t\tfile := model.Object{\n\t\t\tPath:     path.Join(fullPath, f.Name()),\n\t\t\tName:     f.Name(),\n\t\t\tModified: f.ModTime(),\n\t\t\tSize:     f.Size(),\n\t\t\tIsFolder: f.IsDir(),\n\t\t\tCtime:    f.(*smb2.FileStat).CreationTime,\n\t\t}\n\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tfullPath := file.GetPath()\n\tremoteFile, err := d.fs.Open(fullPath)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn nil, err\n\t}\n\td.updateLastConnTime()\n\tmFile := &stream.RateLimitFile{\n\t\tFile:    remoteFile,\n\t\tLimiter: stream.ServerDownloadLimit,\n\t\tCtx:     ctx,\n\t}\n\treturn &model.Link{\n\t\tRangeReader:      stream.GetRangeReaderFromMFile(file.GetSize(), mFile),\n\t\tSyncClosers:      utils.NewSyncClosers(remoteFile),\n\t\tRequireReference: true,\n\t}, nil\n}\n\nfunc (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tfullPath := filepath.Join(parentDir.GetPath(), dirName)\n\terr := d.fs.MkdirAll(fullPath, 0700)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn nil\n}\n\nfunc (d *SMB) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())\n\terr := d.fs.Rename(srcPath, dstPath)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn nil\n}\n\nfunc (d *SMB) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(filepath.Dir(srcPath), newName)\n\terr := d.fs.Rename(srcPath, dstPath)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn nil\n}\n\nfunc (d *SMB) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tsrcPath := srcObj.GetPath()\n\tdstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())\n\tvar err error\n\tif srcObj.IsDir() {\n\t\terr = d.CopyDir(srcPath, dstPath)\n\t} else {\n\t\terr = d.CopyFile(srcPath, dstPath)\n\t}\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn nil\n}\n\nfunc (d *SMB) Remove(ctx context.Context, obj model.Obj) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tvar err error\n\tfullPath := obj.GetPath()\n\tif obj.IsDir() {\n\t\terr = d.fs.RemoveAll(fullPath)\n\t} else {\n\t\terr = d.fs.Remove(fullPath)\n\t}\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn nil\n}\n\nfunc (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn err\n\t}\n\tfullPath := filepath.Join(dstDir.GetPath(), stream.GetName())\n\tout, err := d.fs.Create(fullPath)\n\tif err != nil {\n\t\td.cleanLastConnTime()\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\tdefer func() {\n\t\t_ = out.Close()\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\t_ = d.fs.Remove(fullPath)\n\t\t}\n\t}()\n\terr = utils.CopyWithCtx(ctx, out, driver.NewLimitedUploadStream(ctx, stream), stream.GetSize(), up)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *SMB) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tif err := d.checkConn(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tstat, err := d.fs.Statfs(d.RootFolderPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal := int64(stat.BlockSize() * stat.TotalBlockCount())\n\tfree := int64(stat.BlockSize() * stat.AvailableBlockCount())\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  total - free,\n\t\t},\n\t}, nil\n}\n\n//func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*SMB)(nil)\n"
  },
  {
    "path": "drivers/smb/meta.go",
    "content": "package smb\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress   string `json:\"address\" required:\"true\"`\n\tUsername  string `json:\"username\" required:\"true\"`\n\tPassword  string `json:\"password\"`\n\tShareName string `json:\"share_name\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:        \"SMB\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tDefaultRoot: \".\",\n\tNoCache:     true,\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &SMB{}\n\t})\n}\n"
  },
  {
    "path": "drivers/smb/types.go",
    "content": "package smb\n"
  },
  {
    "path": "drivers/smb/util.go",
    "content": "package smb\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/cloudsoda/go-smb2\"\n)\n\nfunc (d *SMB) updateLastConnTime() {\n\tatomic.StoreInt64(&d.lastConnTime, time.Now().Unix())\n}\n\nfunc (d *SMB) cleanLastConnTime() {\n\tatomic.StoreInt64(&d.lastConnTime, 0)\n}\n\nfunc (d *SMB) getLastConnTime() time.Time {\n\treturn time.Unix(atomic.LoadInt64(&d.lastConnTime), 0)\n}\n\nfunc (d *SMB) initFS(ctx context.Context) error {\n\t_, err, _ := singleflight.AnyGroup.Do(fmt.Sprintf(\"SMB.initFS:%p\", d), func() (any, error) {\n\t\treturn nil, d._initFS(ctx)\n\t})\n\treturn err\n}\nfunc (d *SMB) _initFS(ctx context.Context) error {\n\tdialer := &smb2.Dialer{\n\t\tInitiator: &smb2.NTLMInitiator{\n\t\t\tUser:     d.Username,\n\t\t\tPassword: d.Password,\n\t\t},\n\t}\n\ts, err := dialer.Dial(ctx, d.Address)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.fs, err = s.Mount(d.ShareName)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.updateLastConnTime()\n\treturn err\n}\n\nfunc (d *SMB) checkConn(ctx context.Context) error {\n\tif time.Since(d.getLastConnTime()) < 5*time.Minute {\n\t\treturn nil\n\t}\n\tif d.fs != nil {\n\t\t_ = d.fs.Umount()\n\t}\n\treturn d.initFS(ctx)\n}\n\n// CopyFile File copies a single file from src to dst\nfunc (d *SMB) CopyFile(src, dst string) error {\n\tvar err error\n\tvar srcfd *smb2.File\n\tvar dstfd *smb2.File\n\tvar srcinfo fs.FileInfo\n\n\tif srcfd, err = d.fs.Open(src); err != nil {\n\t\treturn err\n\t}\n\tdefer srcfd.Close()\n\n\tif dstfd, err = d.CreateNestedFile(dst); err != nil {\n\t\treturn err\n\t}\n\tdefer dstfd.Close()\n\n\tif _, err = utils.CopyWithBuffer(dstfd, srcfd); err != nil {\n\t\treturn err\n\t}\n\tif srcinfo, err = d.fs.Stat(src); err != nil {\n\t\treturn err\n\t}\n\treturn d.fs.Chmod(dst, srcinfo.Mode())\n}\n\n// CopyDir Dir copies a whole directory recursively\nfunc (d *SMB) CopyDir(src string, dst string) error {\n\tvar err error\n\tvar fds []fs.FileInfo\n\tvar srcinfo fs.FileInfo\n\n\tif srcinfo, err = d.fs.Stat(src); err != nil {\n\t\treturn err\n\t}\n\tif err = d.fs.MkdirAll(dst, srcinfo.Mode()); err != nil {\n\t\treturn err\n\t}\n\tif fds, err = d.fs.ReadDir(src); err != nil {\n\t\treturn err\n\t}\n\tfor _, fd := range fds {\n\t\tsrcfp := filepath.Join(src, fd.Name())\n\t\tdstfp := filepath.Join(dst, fd.Name())\n\n\t\tif fd.IsDir() {\n\t\t\tif err = d.CopyDir(srcfp, dstfp); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err = d.CopyFile(srcfp, dstfp); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Exists determine whether the file exists\nfunc (d *SMB) Exists(name string) bool {\n\tif _, err := d.fs.Stat(name); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// CreateNestedFile create nested file\nfunc (d *SMB) CreateNestedFile(path string) (*smb2.File, error) {\n\tbasePath := filepath.Dir(path)\n\tif !d.Exists(basePath) {\n\t\terr := d.fs.MkdirAll(basePath, 0700)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn d.fs.Create(path)\n}\n"
  },
  {
    "path": "drivers/strm/driver.go",
    "content": "package strm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Strm struct {\n\tmodel.Storage\n\tAddition\n\tpathMap     map[string][]string\n\tautoFlatten bool\n\toneKey      string\n\n\tsupportSuffix  map[string]struct{}\n\tdownloadSuffix map[string]struct{}\n}\n\nfunc (d *Strm) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Strm) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Strm) Init(ctx context.Context) error {\n\tif d.Paths == \"\" {\n\t\treturn errors.New(\"paths is required\")\n\t}\n\tif d.SaveStrmToLocal && len(d.SaveStrmLocalPath) <= 0 {\n\t\treturn errors.New(\"SaveStrmLocalPath is required\")\n\t}\n\td.pathMap = make(map[string][]string)\n\tfor _, path := range strings.Split(d.Paths, \"\\n\") {\n\t\tpath = strings.TrimSpace(path)\n\t\tif path == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tk, v := getPair(path)\n\t\td.pathMap[k] = append(d.pathMap[k], v)\n\t\tif d.SaveStrmToLocal {\n\t\t\terr := InsertStrm(utils.FixAndCleanPath(strings.TrimSpace(path)), d)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"insert strmTrie error: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\tif len(d.pathMap) == 1 {\n\t\tfor k := range d.pathMap {\n\t\t\td.oneKey = k\n\t\t}\n\t\td.autoFlatten = true\n\t} else {\n\t\td.oneKey = \"\"\n\t\td.autoFlatten = false\n\t}\n\n\tvar supportTypes []string\n\tif d.FilterFileTypes == \"\" {\n\t\td.FilterFileTypes = \"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac\"\n\t}\n\tsupportTypes = strings.Split(d.FilterFileTypes, \",\")\n\td.supportSuffix = map[string]struct{}{}\n\tfor _, ext := range supportTypes {\n\t\text = strings.ToLower(strings.TrimSpace(ext))\n\t\tif ext != \"\" {\n\t\t\td.supportSuffix[ext] = struct{}{}\n\t\t}\n\t}\n\n\tvar downloadTypes []string\n\tif d.DownloadFileTypes == \"\" {\n\t\td.DownloadFileTypes = \"ass,srt,vtt,sub,strm\"\n\t}\n\tdownloadTypes = strings.Split(d.DownloadFileTypes, \",\")\n\td.downloadSuffix = map[string]struct{}{}\n\tfor _, ext := range downloadTypes {\n\t\text = strings.ToLower(strings.TrimSpace(ext))\n\t\tif ext != \"\" {\n\t\t\td.downloadSuffix[ext] = struct{}{}\n\t\t}\n\t}\n\n\tif d.Version != 5 {\n\t\ttypes := strings.Split(\"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac\", \",\")\n\t\tfor _, ext := range types {\n\t\t\tif _, ok := d.supportSuffix[ext]; !ok {\n\t\t\t\td.supportSuffix[ext] = struct{}{}\n\t\t\t\tsupportTypes = append(supportTypes, ext)\n\t\t\t}\n\t\t}\n\t\td.FilterFileTypes = strings.Join(supportTypes, \",\")\n\n\t\ttypes = strings.Split(\"ass,srt,vtt,sub,strm\", \",\")\n\t\tfor _, ext := range types {\n\t\t\tif _, ok := d.downloadSuffix[ext]; !ok {\n\t\t\t\td.downloadSuffix[ext] = struct{}{}\n\t\t\t\tdownloadTypes = append(downloadTypes, ext)\n\t\t\t}\n\t\t}\n\t\td.DownloadFileTypes = strings.Join(downloadTypes, \",\")\n\t\td.PathPrefix = \"/d\"\n\t\td.Version = 5\n\t}\n\tif len(d.SaveLocalMode) == 0 {\n\t\td.SaveLocalMode = SaveLocalInsertMode\n\t}\n\treturn nil\n}\n\nfunc (d *Strm) Drop(ctx context.Context) error {\n\td.pathMap = nil\n\td.downloadSuffix = nil\n\td.supportSuffix = nil\n\tfor _, path := range strings.Split(d.Paths, \"\\n\") {\n\t\tRemoveStrm(utils.FixAndCleanPath(strings.TrimSpace(path)), d)\n\t}\n\treturn nil\n}\n\nfunc (Addition) GetRootPath() string {\n\treturn \"/\"\n}\n\nfunc (d *Strm) Get(ctx context.Context, path string) (model.Obj, error) {\n\troot, sub := d.getRootAndPath(path)\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor _, dst := range dsts {\n\t\treqPath := stdpath.Join(dst, sub)\n\t\tobj, err := fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\t// fs.Get 没报错，说明不是strm驱动映射的路径，需要直接返回\n\t\tsize := int64(0)\n\t\tif !obj.IsDir() {\n\t\t\tsize = obj.GetSize()\n\t\t\tpath = reqPath //把路径设置为真实的，供Link直接读取\n\t\t}\n\t\treturn &model.Object{\n\t\t\tPath:     path,\n\t\t\tName:     obj.GetName(),\n\t\t\tSize:     size,\n\t\t\tModified: obj.ModTime(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t\tHashInfo: obj.GetHash(),\n\t\t}, nil\n\t}\n\tif strings.HasSuffix(path, \".strm\") {\n\t\t// 上面fs.Get都没找到且后缀为.strm\n\t\t// 返回errs.NotSupport使得op.Get尝试从op.List中查找\n\t\treturn nil, errs.NotSupport\n\t}\n\treturn nil, errs.ObjectNotFound\n}\n\nfunc (d *Strm) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tpath := dir.GetPath()\n\tif utils.PathEqual(path, \"/\") && !d.autoFlatten {\n\t\treturn d.listRoot(), nil\n\t}\n\troot, sub := d.getRootAndPath(path)\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tvar objs []model.Obj\n\tfsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}\n\tfor _, dst := range dsts {\n\t\ttmp, err := d.list(ctx, dst, sub, fsArgs)\n\t\tif err == nil {\n\t\t\tobjs = append(objs, tmp...)\n\t\t}\n\t}\n\treturn objs, nil\n}\n\nfunc (d *Strm) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.GetID() == \"strm\" {\n\t\tlink := d.getLink(ctx, file.GetPath())\n\t\treturn &model.Link{\n\t\t\tRangeReader: stream.GetRangeReaderFromMFile(int64(len(link)), strings.NewReader(link)),\n\t\t}, nil\n\t}\n\t// ftp,s3\n\tif common.GetApiUrl(ctx) == \"\" {\n\t\targs.Redirect = false\n\t}\n\treqPath := file.GetPath()\n\tlink, _, err := d.link(ctx, reqPath, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif link == nil {\n\t\treturn &model.Link{\n\t\t\tURL: fmt.Sprintf(\"%s/p%s?sign=%s\",\n\t\t\t\tcommon.GetApiUrl(ctx),\n\t\t\t\tutils.EncodePath(reqPath, true),\n\t\t\t\tsign.Sign(reqPath)),\n\t\t}, nil\n\t}\n\n\tresultLink := *link\n\tresultLink.SyncClosers = utils.NewSyncClosers(link)\n\treturn &resultLink, nil\n}\n\nvar _ driver.Driver = (*Strm)(nil)\n"
  },
  {
    "path": "drivers/strm/hook.go",
    "content": "package strm\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tchap/go-patricia/v2/patricia\"\n)\n\nvar strmTrie = patricia.NewTrie()\n\nfunc UpdateLocalStrm(ctx context.Context, path string, objs []model.Obj) {\n\tpath = utils.FixAndCleanPath(path)\n\tupdateLocal := func(driver *Strm, basePath string, objs []model.Obj) {\n\t\trelParent := strings.TrimPrefix(basePath, utils.GetActualMountPath(driver.MountPath))\n\t\tlocalParentPath := stdpath.Join(driver.SaveStrmLocalPath, relParent)\n\t\tfor _, obj := range objs {\n\t\t\tlocalPath := stdpath.Join(localParentPath, obj.GetName())\n\t\t\tgenerateStrm(ctx, driver, obj, localPath)\n\t\t}\n\t\tdeleteExtraFiles(driver, localParentPath, objs)\n\t}\n\n\t_ = strmTrie.VisitPrefixes(patricia.Prefix(path), func(needPathPrefix patricia.Prefix, item patricia.Item) error {\n\t\tstrmDrivers := item.([]*Strm)\n\t\tneedPath := string(needPathPrefix)\n\t\trestPath := strings.TrimPrefix(path, needPath)\n\t\tif len(restPath) > 0 && restPath[0] != '/' {\n\t\t\treturn nil\n\t\t}\n\t\tfor _, strmDriver := range strmDrivers {\n\t\t\tstrmObjs := strmDriver.convert2strmObjs(ctx, path, objs)\n\t\t\tupdateLocal(strmDriver, stdpath.Join(stdpath.Base(needPath), restPath), strmObjs)\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc InsertStrm(dstPath string, d *Strm) error {\n\tprefix := patricia.Prefix(strings.TrimRight(dstPath, \"/\"))\n\texisting := strmTrie.Get(prefix)\n\n\tif existing == nil {\n\t\tif !strmTrie.Insert(prefix, []*Strm{d}) {\n\t\t\treturn errors.New(\"failed to insert strm\")\n\t\t}\n\t\treturn nil\n\t}\n\tif lst, ok := existing.([]*Strm); ok {\n\t\tstrmTrie.Set(prefix, append(lst, d))\n\t} else {\n\t\treturn errors.New(\"invalid trie item type\")\n\t}\n\n\treturn nil\n}\n\nfunc RemoveStrm(dstPath string, d *Strm) {\n\tprefix := patricia.Prefix(strings.TrimRight(dstPath, \"/\"))\n\texisting := strmTrie.Get(prefix)\n\tif existing == nil {\n\t\treturn\n\t}\n\tlst, ok := existing.([]*Strm)\n\tif !ok {\n\t\treturn\n\t}\n\tif len(lst) == 1 && lst[0] == d {\n\t\tstrmTrie.Delete(prefix)\n\t\treturn\n\t}\n\n\tfor i, di := range lst {\n\t\tif di == d {\n\t\t\tnewList := append(lst[:i], lst[i+1:]...)\n\t\t\tstrmTrie.Set(prefix, newList)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath string) {\n\tif !obj.IsDir() {\n\t\tif utils.Exists(localPath) && driver.SaveLocalMode == SaveLocalInsertMode {\n\t\t\treturn\n\t\t}\n\t\tlink, err := driver.Link(ctx, obj, model.LinkArgs{})\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: failed to link: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\tdefer link.Close()\n\t\tsize := link.ContentLength\n\t\tif size <= 0 {\n\t\t\tsize = obj.GetSize()\n\t\t}\n\t\trrf, err := stream.GetRangeReaderFromLink(size, link)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: failed to get range reader: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\trc, err := rrf.RangeRead(ctx, http_range.Range{Length: -1})\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: failed to read range: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\tdefer rc.Close()\n\t\tsame, err := isSameContent(localPath, size, rc)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to compare content of obj %s: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\tif same {\n\t\t\treturn\n\t\t}\n\t\trc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1})\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: failed to reread range: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\tdefer rc.Close()\n\t\tfile, err := utils.CreateNestedFile(localPath)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: failed to create local file: %v\", localPath, err)\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\t\tif _, err := utils.CopyWithBuffer(file, rc); err != nil {\n\t\t\tlog.Warnf(\"failed to generate strm of obj %s: copy failed: %v\", localPath, err)\n\t\t}\n\t}\n}\n\nfunc isSameContent(localPath string, size int64, rc io.Reader) (bool, error) {\n\tinfo, err := os.Stat(localPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\n\tif info.Size() != size {\n\t\treturn false, nil\n\t}\n\tlocalFile, err := os.Open(localPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer localFile.Close()\n\th1 := sha256.New()\n\th2 := sha256.New()\n\tif _, err := io.Copy(h1, localFile); err != nil {\n\t\treturn false, err\n\t}\n\tif _, err := io.Copy(h2, rc); err != nil {\n\t\treturn false, err\n\t}\n\treturn bytes.Equal(h1.Sum(nil), h2.Sum(nil)), nil\n}\n\nfunc deleteExtraFiles(driver *Strm, localPath string, objs []model.Obj) {\n\tif driver.SaveLocalMode != SaveLocalSyncMode {\n\t\treturn\n\t}\n\tlocalFiles, localDirs, err := getLocalDirsAndFiles(localPath)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to read local files from %s: %v\", localPath, err)\n\t\treturn\n\t}\n\n\tfileSet := make(map[string]struct{})\n\tdirSet := make(map[string]struct{})\n\tfor _, obj := range objs {\n\t\tobjPath := stdpath.Join(localPath, obj.GetName())\n\t\tif obj.IsDir() {\n\t\t\tdirSet[objPath] = struct{}{}\n\t\t} else {\n\t\t\tfileSet[objPath] = struct{}{}\n\t\t}\n\t}\n\n\tfor _, localFile := range localFiles {\n\t\tif _, exists := fileSet[localFile]; !exists {\n\t\t\terr := os.Remove(localFile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"Failed to delete file: %s, error: %v\\n\", localFile, err)\n\t\t\t} else {\n\t\t\t\tlog.Infof(\"Deleted file %s\", localFile)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, localDir := range localDirs {\n\t\tif _, exists := dirSet[localDir]; !exists {\n\t\t\terr := os.RemoveAll(localDir)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"Failed to delete directory: %s, error: %v\\n\", localDir, err)\n\t\t\t} else {\n\t\t\t\tlog.Infof(\"Deleted directory %s\", localDir)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc getLocalDirsAndFiles(localPath string) ([]string, []string, error) {\n\tvar files, dirs []string\n\tentries, err := os.ReadDir(localPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor _, entry := range entries {\n\t\tfullPath := stdpath.Join(localPath, entry.Name())\n\t\tif entry.IsDir() {\n\t\t\tdirs = append(dirs, fullPath)\n\t\t} else {\n\t\t\tfiles = append(files, fullPath)\n\t\t}\n\t}\n\treturn files, dirs, nil\n}\n\nfunc init() {\n\top.RegisterObjsUpdateHook(UpdateLocalStrm)\n}\n"
  },
  {
    "path": "drivers/strm/meta.go",
    "content": "package strm\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\nconst (\n\tSaveLocalInsertMode = \"insert\"\n\tSaveLocalUpdateMode = \"update\"\n\tSaveLocalSyncMode   = \"sync\"\n)\n\ntype Addition struct {\n\tPaths             string `json:\"paths\" required:\"true\" type:\"text\"`\n\tSiteUrl           string `json:\"siteUrl\" type:\"text\" required:\"false\" help:\"The prefix URL of the strm file\"`\n\tPathPrefix        string `json:\"PathPrefix\" type:\"text\" required:\"false\" default:\"/d\"  help:\"Path prefix\"`\n\tDownloadFileTypes string `json:\"downloadFileTypes\" type:\"text\" default:\"ass,srt,vtt,sub,strm\" required:\"false\" help:\"Files need to download with strm (usally subtitles)\"`\n\tFilterFileTypes   string `json:\"filterFileTypes\" type:\"text\" default:\"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac\" required:\"false\" help:\"Supports suffix name of strm file\"`\n\tEncodePath        bool   `json:\"encodePath\" default:\"true\" required:\"true\" help:\"encode the path in the strm file\"`\n\tWithoutUrl        bool   `json:\"withoutUrl\" default:\"false\" help:\"strm file content without URL prefix\"`\n\tWithSign          bool   `json:\"withSign\" default:\"false\"`\n\tSaveStrmToLocal   bool   `json:\"SaveStrmToLocal\" default:\"false\" help:\"save strm file locally\"`\n\tSaveStrmLocalPath string `json:\"SaveStrmLocalPath\" type:\"text\" help:\"save strm file local path\"`\n\tSaveLocalMode     string `json:\"SaveLocalMode\" type:\"select\" help:\"save strm file locally mode\" options:\"insert,update,sync\" default:\"insert\"`\n\tVersion           int\n}\n\nvar config = driver.Config{\n\tName:        \"Strm\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tNoCache:     true,\n\tNoUpload:    true,\n\tDefaultRoot: \"/\",\n\tNoLinkURL:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Strm{\n\t\t\tAddition: Addition{\n\t\t\t\tEncodePath: true,\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/strm/util.go",
    "content": "package strm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\nfunc (d *Strm) listRoot() []model.Obj {\n\tvar objs []model.Obj\n\tfor k := range d.pathMap {\n\t\tobj := model.Object{\n\t\t\tPath:     \"/\" + k,\n\t\t\tName:     k,\n\t\t\tIsFolder: true,\n\t\t\tModified: d.Modified,\n\t\t}\n\t\tobjs = append(objs, &obj)\n\t}\n\treturn objs\n}\n\n// do others that not defined in Driver interface\nfunc getPair(path string) (string, string) {\n\t//path = strings.TrimSpace(path)\n\tif strings.Contains(path, \":\") {\n\t\tpair := strings.SplitN(path, \":\", 2)\n\t\tif !strings.Contains(pair[0], \"/\") {\n\t\t\treturn pair[0], pair[1]\n\t\t}\n\t}\n\treturn stdpath.Base(path), path\n}\n\nfunc (d *Strm) getRootAndPath(path string) (string, string) {\n\tif d.autoFlatten {\n\t\treturn d.oneKey, path\n\t}\n\tpath = strings.TrimPrefix(path, \"/\")\n\tparts := strings.SplitN(path, \"/\", 2)\n\tif len(parts) == 1 {\n\t\treturn parts[0], \"\"\n\t}\n\treturn parts[0], parts[1]\n}\n\nfunc (d *Strm) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {\n\treqPath := stdpath.Join(dst, sub)\n\tobjs, err := fs.List(ctx, reqPath, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.convert2strmObjs(ctx, reqPath, objs), nil\n}\n\nfunc (d *Strm) convert2strmObjs(ctx context.Context, reqPath string, objs []model.Obj) []model.Obj {\n\tvar validObjs []model.Obj\n\tfor _, obj := range objs {\n\t\tid, name, path := \"\", obj.GetName(), \"\"\n\t\tsize := int64(0)\n\t\tif !obj.IsDir() {\n\t\t\tpath = stdpath.Join(reqPath, obj.GetName())\n\t\t\tsourceExt := utils.SourceExt(name)\n\t\t\text := strings.ToLower(sourceExt)\n\t\t\tif _, ok := d.downloadSuffix[ext]; ok {\n\t\t\t\tsize = obj.GetSize()\n\t\t\t} else if _, ok := d.supportSuffix[ext]; ok {\n\t\t\t\tid = \"strm\"\n\t\t\t\tname = strings.TrimSuffix(name, sourceExt) + \"strm\"\n\t\t\t\tsize = int64(len(d.getLink(ctx, path)))\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tobjRes := model.Object{\n\t\t\tID:       id,\n\t\t\tPath:     path,\n\t\t\tName:     name,\n\t\t\tSize:     size,\n\t\t\tModified: obj.ModTime(),\n\t\t\tIsFolder: obj.IsDir(),\n\t\t}\n\t\tthumb, ok := model.GetThumb(obj)\n\t\tif !ok {\n\t\t\tvalidObjs = append(validObjs, &objRes)\n\t\t\tcontinue\n\t\t}\n\t\tvalidObjs = append(validObjs, &model.ObjThumb{\n\t\t\tObject: objRes,\n\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\tThumbnail: thumb,\n\t\t\t},\n\t\t})\n\t}\n\treturn validObjs\n}\n\nfunc (d *Strm) getLink(ctx context.Context, path string) string {\n\tfinalPath := path\n\tif d.EncodePath {\n\t\tfinalPath = utils.EncodePath(path, true)\n\t}\n\tif d.WithSign {\n\t\tsignPath := sign.Sign(path)\n\t\tfinalPath = fmt.Sprintf(\"%s?sign=%s\", finalPath, signPath)\n\t}\n\tpathPrefix := d.PathPrefix\n\tif len(pathPrefix) > 0 {\n\t\tfinalPath = stdpath.Join(pathPrefix, finalPath)\n\t}\n\tif !strings.HasPrefix(finalPath, \"/\") {\n\t\tfinalPath = \"/\" + finalPath\n\t}\n\tif d.WithoutUrl {\n\t\treturn finalPath\n\t}\n\tapiUrl := d.SiteUrl\n\tif len(apiUrl) > 0 {\n\t\tapiUrl = strings.TrimSuffix(apiUrl, \"/\")\n\t} else {\n\t\tapiUrl = common.GetApiUrl(ctx)\n\t}\n\treturn fmt.Sprintf(\"%s%s\",\n\t\tapiUrl,\n\t\tfinalPath)\n}\n\nfunc (d *Strm) link(ctx context.Context, reqPath string, args model.LinkArgs) (*model.Link, model.Obj, error) {\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif !args.Redirect {\n\t\treturn op.Link(ctx, storage, reqActualPath, args)\n\t}\n\tobj, err := fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif common.ShouldProxy(storage, stdpath.Base(reqPath)) {\n\t\treturn nil, obj, nil\n\t}\n\treturn op.Link(ctx, storage, reqActualPath, args)\n}\n"
  },
  {
    "path": "drivers/teambition/driver.go",
    "content": "package teambition\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Teambition struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Teambition) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Teambition) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Teambition) Init(ctx context.Context) error {\n\t_, err := d.request(\"/api/v2/roles\", http.MethodGet, nil, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Teambition) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn d.getFiles(dir.GetID())\n}\n\nfunc (d *Teambition) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif u, ok := file.(model.URL); ok {\n\t\turl := u.URL()\n\t\tres, _ := base.NoRedirectClient.R().Get(url)\n\t\tif res.StatusCode() == 302 {\n\t\t\turl = res.Header().Get(\"location\")\n\t\t}\n\t\treturn &model.Link{URL: url}, nil\n\t}\n\treturn nil, errors.New(\"can't convert obj to URL\")\n}\n\nfunc (d *Teambition) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdata := base.Json{\n\t\t\"objectType\":     \"collection\",\n\t\t\"_projectId\":     d.ProjectID,\n\t\t\"_creatorId\":     \"\",\n\t\t\"created\":        \"\",\n\t\t\"updated\":        \"\",\n\t\t\"title\":          dirName,\n\t\t\"color\":          \"blue\",\n\t\t\"description\":    \"\",\n\t\t\"workCount\":      0,\n\t\t\"collectionType\": \"\",\n\t\t\"recentWorks\":    []interface{}{},\n\t\t\"_parentId\":      parentDir.GetID(),\n\t\t\"subCount\":       nil,\n\t}\n\t_, err := d.request(\"/api/collections\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tpre := \"/api/works/\"\n\tif srcObj.IsDir() {\n\t\tpre = \"/api/collections/\"\n\t}\n\t_, err := d.request(pre+srcObj.GetID()+\"/move\", http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"_parentId\": dstDir.GetID(),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tpre := \"/api/works/\"\n\tdata := base.Json{\n\t\t\"fileName\": newName,\n\t}\n\tif srcObj.IsDir() {\n\t\tpre = \"/api/collections/\"\n\t\tdata = base.Json{\n\t\t\t\"title\": newName,\n\t\t}\n\t}\n\t_, err := d.request(pre+srcObj.GetID(), http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(data)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tpre := \"/api/works/\"\n\tif srcObj.IsDir() {\n\t\tpre = \"/api/collections/\"\n\t}\n\t_, err := d.request(pre+srcObj.GetID()+\"/fork\", http.MethodPut, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"_parentId\": dstDir.GetID(),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Remove(ctx context.Context, obj model.Obj) error {\n\tpre := \"/api/works/\"\n\tif obj.IsDir() {\n\t\tpre = \"/api/collections/\"\n\t}\n\t_, err := d.request(pre+obj.GetID()+\"/archive\", http.MethodPost, nil, nil)\n\treturn err\n}\n\nfunc (d *Teambition) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif d.UseS3UploadMethod {\n\t\treturn d.newUpload(ctx, dstDir, stream, up)\n\t}\n\tvar (\n\t\ttoken string\n\t\terr   error\n\t)\n\tif d.isInternational() {\n\t\tres, err := d.request(\"/projects\", http.MethodGet, nil, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoken = getBetweenStr(string(res), \"strikerAuth&quot;:&quot;\", \"&quot;,&quot;phoneForLogin\")\n\t} else {\n\t\tres, err := d.request(\"/api/v2/users/me\", http.MethodGet, nil, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoken = utils.Json.Get(res, \"strikerAuth\").ToString()\n\t}\n\tvar newFile *FileUpload\n\tif stream.GetSize() <= 20971520 {\n\t\t// post upload\n\t\tnewFile, err = d.upload(ctx, stream, token, up)\n\t} else {\n\t\t// chunk upload\n\t\t//err = base.ErrNotImplement\n\t\tnewFile, err = d.chunkUpload(ctx, stream, token, up)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.finishUpload(newFile, dstDir.GetID())\n}\n\nvar _ driver.Driver = (*Teambition)(nil)\n"
  },
  {
    "path": "drivers/teambition/help.go",
    "content": "package teambition\n\nimport \"strings\"\n\nfunc getBetweenStr(str, start, end string) string {\n\tn := strings.Index(str, start)\n\tif n == -1 {\n\t\treturn \"\"\n\t}\n\tn = n + len(start)\n\tstr = string([]byte(str)[n:])\n\tm := strings.Index(str, end)\n\tif m == -1 {\n\t\treturn \"\"\n\t}\n\tstr = string([]byte(str)[:m])\n\treturn str\n}\n"
  },
  {
    "path": "drivers/teambition/meta.go",
    "content": "package teambition\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tRegion    string `json:\"region\" type:\"select\" options:\"china,international\" required:\"true\"`\n\tCookie    string `json:\"cookie\" required:\"true\"`\n\tProjectID string `json:\"project_id\" required:\"true\"`\n\tdriver.RootID\n\tOrderBy           string `json:\"order_by\" type:\"select\" options:\"fileName,fileSize,updated,created\" default:\"fileName\"`\n\tOrderDirection    string `json:\"order_direction\" type:\"select\" options:\"Asc,Desc\" default:\"Asc\"`\n\tUseS3UploadMethod bool   `json:\"use_s3_upload_method\" default:\"true\"`\n}\n\nvar config = driver.Config{\n\tName: \"Teambition\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Teambition{}\n\t})\n}\n"
  },
  {
    "path": "drivers/teambition/types.go",
    "content": "package teambition\n\nimport \"time\"\n\ntype ErrResp struct {\n\tName    string `json:\"name\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Collection struct {\n\tID      string    `json:\"_id\"`\n\tTitle   string    `json:\"title\"`\n\tUpdated time.Time `json:\"updated\"`\n}\n\ntype Work struct {\n\tID           string    `json:\"_id\"`\n\tFileName     string    `json:\"fileName\"`\n\tFileSize     int64     `json:\"fileSize\"`\n\tFileKey      string    `json:\"fileKey\"`\n\tFileCategory string    `json:\"fileCategory\"`\n\tDownloadURL  string    `json:\"downloadUrl\"`\n\tThumbnailURL string    `json:\"thumbnailUrl\"`\n\tThumbnail    string    `json:\"thumbnail\"`\n\tUpdated      time.Time `json:\"updated\"`\n\tPreviewURL   string    `json:\"previewUrl\"`\n}\n\ntype FileUpload struct {\n\tFileKey        string        `json:\"fileKey\"`\n\tFileName       string        `json:\"fileName\"`\n\tFileType       string        `json:\"fileType\"`\n\tFileSize       int           `json:\"fileSize\"`\n\tFileCategory   string        `json:\"fileCategory\"`\n\tImageWidth     int           `json:\"imageWidth\"`\n\tImageHeight    int           `json:\"imageHeight\"`\n\tInvolveMembers []interface{} `json:\"involveMembers\"`\n\tSource         string        `json:\"source\"`\n\tVisible        string        `json:\"visible\"`\n\tParentId       string        `json:\"_parentId\"`\n}\n\ntype ChunkUpload struct {\n\tFileUpload\n\tStorage        string        `json:\"storage\"`\n\tMimeType       string        `json:\"mimeType\"`\n\tChunks         int           `json:\"chunks\"`\n\tChunkSize      int           `json:\"chunkSize\"`\n\tCreated        time.Time     `json:\"created\"`\n\tFileMD5        string        `json:\"fileMD5\"`\n\tLastUpdated    time.Time     `json:\"lastUpdated\"`\n\tUploadedChunks []interface{} `json:\"uploadedChunks\"`\n\tToken          struct {\n\t\tAppID          string    `json:\"AppID\"`\n\t\tOrganizationID string    `json:\"OrganizationID\"`\n\t\tUserID         string    `json:\"UserID\"`\n\t\tExp            time.Time `json:\"Exp\"`\n\t\tStorage        string    `json:\"Storage\"`\n\t\tResource       string    `json:\"Resource\"`\n\t\tSpeed          int       `json:\"Speed\"`\n\t} `json:\"token\"`\n\tDownloadUrl    string      `json:\"downloadUrl\"`\n\tThumbnailUrl   string      `json:\"thumbnailUrl\"`\n\tPreviewUrl     string      `json:\"previewUrl\"`\n\tImmPreviewUrl  string      `json:\"immPreviewUrl\"`\n\tPreviewExt     string      `json:\"previewExt\"`\n\tLastUploadTime interface{} `json:\"lastUploadTime\"`\n}\n\ntype UploadToken struct {\n\tSdk struct {\n\t\tEndpoint         string `json:\"endpoint\"`\n\t\tRegion           string `json:\"region\"`\n\t\tS3ForcePathStyle bool   `json:\"s3ForcePathStyle\"`\n\t\tCredentials      struct {\n\t\t\tAccessKeyId     string `json:\"accessKeyId\"`\n\t\t\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t\t\tSessionToken    string `json:\"sessionToken\"`\n\t\t} `json:\"credentials\"`\n\t} `json:\"sdk\"`\n\tUpload struct {\n\t\tBucket             string `json:\"Bucket\"`\n\t\tKey                string `json:\"Key\"`\n\t\tContentDisposition string `json:\"ContentDisposition\"`\n\t\tContentType        string `json:\"ContentType\"`\n\t} `json:\"upload\"`\n\tToken       string `json:\"token\"`\n\tDownloadUrl string `json:\"downloadUrl\"`\n}\n"
  },
  {
    "path": "drivers/teambition/util.go",
    "content": "package teambition\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *Teambition) isInternational() bool {\n\treturn d.Region == \"international\"\n}\n\nfunc (d *Teambition) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\turl := \"https://www.teambition.com\" + pathname\n\tif d.isInternational() {\n\t\turl = \"https://us.teambition.com\" + pathname\n\t}\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Cookie\", d.Cookie)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Name != \"\" {\n\t\treturn nil, errors.New(e.Message)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Teambition) getFiles(parentId string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tpage := 1\n\tfor {\n\t\tvar collections []Collection\n\t\t_, err := d.request(\"/api/collections\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"_parentId\":  parentId,\n\t\t\t\t\"_projectId\": d.ProjectID,\n\t\t\t\t\"order\":      d.OrderBy + d.OrderDirection,\n\t\t\t\t\"count\":      \"50\",\n\t\t\t\t\"page\":       strconv.Itoa(page),\n\t\t\t})\n\t\t}, &collections)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(collections) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t\tfor _, collection := range collections {\n\t\t\tif collection.Title == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfiles = append(files, &model.Object{\n\t\t\t\tID:       collection.ID,\n\t\t\t\tName:     collection.Title,\n\t\t\t\tIsFolder: true,\n\t\t\t\tModified: collection.Updated,\n\t\t\t})\n\t\t}\n\t}\n\tpage = 1\n\tfor {\n\t\tvar works []Work\n\t\t_, err := d.request(\"/api/works\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\"_parentId\":  parentId,\n\t\t\t\t\"_projectId\": d.ProjectID,\n\t\t\t\t\"order\":      d.OrderBy + d.OrderDirection,\n\t\t\t\t\"count\":      \"50\",\n\t\t\t\t\"page\":       strconv.Itoa(page),\n\t\t\t})\n\t\t}, &works)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(works) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t\tfor _, work := range works {\n\t\t\tfiles = append(files, &model.ObjThumbURL{\n\t\t\t\tObject: model.Object{\n\t\t\t\t\tID:       work.ID,\n\t\t\t\t\tName:     work.FileName,\n\t\t\t\t\tSize:     work.FileSize,\n\t\t\t\t\tModified: work.Updated,\n\t\t\t\t},\n\t\t\t\tThumbnail: model.Thumbnail{Thumbnail: work.Thumbnail},\n\t\t\t\tUrl:       model.Url{Url: work.DownloadURL},\n\t\t\t})\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (d *Teambition) upload(ctx context.Context, file model.FileStreamer, token string, up driver.UpdateProgress) (*FileUpload, error) {\n\tprefix := \"tcs\"\n\tif d.isInternational() {\n\t\tprefix = \"us-tcs\"\n\t}\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         file,\n\t\tUpdateProgress: up,\n\t})\n\tvar newFile FileUpload\n\tres, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetResult(&newFile).SetHeader(\"Authorization\", token).\n\t\tSetMultipartFormData(map[string]string{\n\t\t\t\"name\":             file.GetName(),\n\t\t\t\"type\":             file.GetMimetype(),\n\t\t\t\"size\":             strconv.FormatInt(file.GetSize(), 10),\n\t\t\t\"lastModifiedDate\": time.Now().Format(\"Mon Jan 02 2006 15:04:05 GMT+0800 (中国标准时间)\"),\n\t\t}).\n\t\tSetMultipartField(\"file\", file.GetName(), file.GetMimetype(), reader).\n\t\tPost(fmt.Sprintf(\"https://%s.teambition.net/upload\", prefix))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"[teambition] upload response: %s\", res.String())\n\treturn &newFile, nil\n}\n\nfunc (d *Teambition) chunkUpload(ctx context.Context, file model.FileStreamer, token string, up driver.UpdateProgress) (*FileUpload, error) {\n\tprefix := \"tcs\"\n\treferer := \"https://www.teambition.com/\"\n\tif d.isInternational() {\n\t\tprefix = \"us-tcs\"\n\t\treferer = \"https://us.teambition.com/\"\n\t}\n\tvar newChunk ChunkUpload\n\t_, err := base.RestyClient.R().SetResult(&newChunk).SetHeader(\"Authorization\", token).\n\t\tSetBody(base.Json{\n\t\t\t\"fileName\":    file.GetName(),\n\t\t\t\"fileSize\":    file.GetSize(),\n\t\t\t\"lastUpdated\": time.Now(),\n\t\t}).Post(fmt.Sprintf(\"https://%s.teambition.net/upload/chunk\", prefix))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor i := 0; i < newChunk.Chunks; i++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t\tchunkSize := newChunk.ChunkSize\n\t\tif i == newChunk.Chunks-1 {\n\t\t\tchunkSize = int(file.GetSize()) - i*chunkSize\n\t\t}\n\t\tlog.Debugf(\"%d : %d\", i, chunkSize)\n\t\tchunkData := make([]byte, chunkSize)\n\t\t_, err = io.ReadFull(file, chunkData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tu := fmt.Sprintf(\"https://%s.teambition.net/upload/chunk/%s?chunk=%d&chunks=%d\",\n\t\t\tprefix, newChunk.FileKey, i+1, newChunk.Chunks)\n\t\tlog.Debugf(\"url: %s\", u)\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetContext(ctx).\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\t\"Authorization\": token,\n\t\t\t\t\"Content-Type\":  \"application/octet-stream\",\n\t\t\t\t\"Referer\":       referer,\n\t\t\t}).\n\t\t\tSetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(chunkData))).\n\t\t\tPost(u)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tup(float64(i) * 100 / float64(newChunk.Chunks))\n\t}\n\t_, err = base.RestyClient.R().SetHeader(\"Authorization\", token).Post(\n\t\tfmt.Sprintf(\"https://%s.teambition.net/upload/chunk/%s\",\n\t\t\tprefix, newChunk.FileKey))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &newChunk.FileUpload, nil\n}\n\nfunc (d *Teambition) finishUpload(file *FileUpload, parentId string) error {\n\tfile.InvolveMembers = []interface{}{}\n\tfile.Visible = \"members\"\n\tfile.ParentId = parentId\n\t_, err := d.request(\"/api/works\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"works\":     []FileUpload{*file},\n\t\t\t\"_parentId\": parentId,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Teambition) newUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tvar uploadToken UploadToken\n\t_, err := d.request(\"/api/awos/upload-token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"category\": \"work\",\n\t\t\t\"fileName\": stream.GetName(),\n\t\t\t\"fileSize\": stream.GetSize(),\n\t\t\t\"fileType\": stream.GetMimetype(),\n\t\t\t\"payload\": base.Json{\n\t\t\t\t\"involveMembers\": []struct{}{},\n\t\t\t\t\"visible\":        \"members\",\n\t\t\t},\n\t\t\t\"scope\": \"project:\" + d.ProjectID,\n\t\t})\n\t}, &uploadToken)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg := &aws.Config{\n\t\tCredentials: credentials.NewStaticCredentials(\n\t\t\tuploadToken.Sdk.Credentials.AccessKeyId, uploadToken.Sdk.Credentials.SecretAccessKey, uploadToken.Sdk.Credentials.SessionToken),\n\t\tRegion:           &uploadToken.Sdk.Region,\n\t\tEndpoint:         &uploadToken.Sdk.Endpoint,\n\t\tS3ForcePathStyle: &uploadToken.Sdk.S3ForcePathStyle,\n\t}\n\tss, err := session.NewSession(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuploader := s3manager.NewUploader(ss)\n\tif stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\tuploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)\n\t}\n\tinput := &s3manager.UploadInput{\n\t\tBucket:             &uploadToken.Upload.Bucket,\n\t\tKey:                &uploadToken.Upload.Key,\n\t\tContentDisposition: &uploadToken.Upload.ContentDisposition,\n\t\tContentType:        &uploadToken.Upload.ContentType,\n\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\tReader:         stream,\n\t\t\tUpdateProgress: up,\n\t\t}),\n\t}\n\t_, err = uploader.UploadWithContext(ctx, input)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// finish upload\n\t_, err = d.request(\"/api/works\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"fileTokens\":     []string{uploadToken.Token},\n\t\t\t\"involveMembers\": []struct{}{},\n\t\t\t\"visible\":        \"members\",\n\t\t\t\"works\":          []struct{}{},\n\t\t\t\"_parentId\":      dstDir.GetID(),\n\t\t})\n\t}, nil)\n\treturn err\n}\n"
  },
  {
    "path": "drivers/teldrive/copy.go",
    "content": "package teldrive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"golang.org/x/net/context\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sync/semaphore\"\n)\n\nfunc NewCopyManager(ctx context.Context, concurrent int, d *Teldrive) *CopyManager {\n\tg, ctx := errgroup.WithContext(ctx)\n\n\treturn &CopyManager{\n\t\tTaskChan: make(chan CopyTask, concurrent*2),\n\t\tSem:      semaphore.NewWeighted(int64(concurrent)),\n\t\tG:        g,\n\t\tCtx:      ctx,\n\t\td:        d,\n\t}\n}\n\nfunc (cm *CopyManager) startWorkers() {\n\tworkerCount := cap(cm.TaskChan) / 2\n\tfor i := 0; i < workerCount; i++ {\n\t\tcm.G.Go(func() error {\n\t\t\treturn cm.worker()\n\t\t})\n\t}\n}\n\nfunc (cm *CopyManager) worker() error {\n\tfor {\n\t\tselect {\n\t\tcase task, ok := <-cm.TaskChan:\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := cm.Sem.Acquire(cm.Ctx, 1); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar err error\n\n\t\t\terr = cm.processFile(task)\n\n\t\t\tcm.Sem.Release(1)\n\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"task processing failed: %w\", err)\n\t\t\t}\n\n\t\tcase <-cm.Ctx.Done():\n\t\t\treturn cm.Ctx.Err()\n\t\t}\n\t}\n}\n\nfunc (cm *CopyManager) generateTasks(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif srcObj.IsDir() {\n\t\treturn cm.generateFolderTasks(ctx, srcObj, dstDir)\n\t} else {\n\t\t// add single file task directly\n\t\tselect {\n\t\tcase cm.TaskChan <- CopyTask{SrcObj: srcObj, DstDir: dstDir}:\n\t\t\treturn nil\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\t}\n\t}\n}\n\nfunc (cm *CopyManager) generateFolderTasks(ctx context.Context, srcDir, dstDir model.Obj) error {\n\tobjs, err := cm.d.List(ctx, srcDir, model.ListArgs{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list directory %s: %w\", srcDir.GetPath(), err)\n\t}\n\n\terr = cm.d.MakeDir(cm.Ctx, dstDir, srcDir.GetName())\n\tif err != nil || len(objs) == 0 {\n\t\treturn err\n\t}\n\tnewDstDir := &model.Object{\n\t\tID:       dstDir.GetID(),\n\t\tPath:     dstDir.GetPath() + \"/\" + srcDir.GetName(),\n\t\tName:     srcDir.GetName(),\n\t\tIsFolder: true,\n\t}\n\n\tfor _, file := range objs {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\n\t\tsrcFile := &model.Object{\n\t\t\tID:       file.GetID(),\n\t\t\tPath:     srcDir.GetPath() + \"/\" + file.GetName(),\n\t\t\tName:     file.GetName(),\n\t\t\tIsFolder: file.IsDir(),\n\t\t}\n\n\t\t// 递归生成任务\n\t\tif err := cm.generateTasks(ctx, srcFile, newDstDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cm *CopyManager) processFile(task CopyTask) error {\n\treturn cm.copySingleFile(cm.Ctx, task.SrcObj, task.DstDir)\n}\n\nfunc (cm *CopyManager) copySingleFile(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// `override copy mode` should delete the existing file\n\tif obj, err := cm.d.getFile(dstDir.GetPath(), srcObj.GetName(), srcObj.IsDir()); err == nil {\n\t\tif err := cm.d.Remove(ctx, obj); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove existing file: %w\", err)\n\t\t}\n\t}\n\n\t// Do copy\n\treturn cm.d.request(http.MethodPost, \"/api/files/{id}/copy\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", srcObj.GetID())\n\t\treq.SetBody(base.Json{\n\t\t\t\"newName\":     srcObj.GetName(),\n\t\t\t\"destination\": dstDir.GetPath(),\n\t\t})\n\t}, nil)\n}\n"
  },
  {
    "path": "drivers/teldrive/driver.go",
    "content": "package teldrive\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype Teldrive struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Teldrive) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Teldrive) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Teldrive) Init(ctx context.Context) error {\n\td.Address = strings.TrimSuffix(d.Address, \"/\")\n\tif d.Cookie == \"\" || !strings.HasPrefix(d.Cookie, \"access_token=\") {\n\t\treturn fmt.Errorf(\"cookie must start with 'access_token='\")\n\t}\n\tif d.UploadConcurrency == 0 {\n\t\td.UploadConcurrency = 4\n\t}\n\tif d.ChunkSize == 0 {\n\t\td.ChunkSize = 10\n\t}\n\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Teldrive) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Teldrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar firstResp ListResp\n\terr := d.request(http.MethodGet, \"/api/files\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"path\":  dir.GetPath(),\n\t\t\t\"limit\": \"500\",\n\t\t\t\"page\":  \"1\",\n\t\t})\n\t}, &firstResp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpagesData := make([][]Object, firstResp.Meta.TotalPages)\n\tpagesData[0] = firstResp.Items\n\n\tif firstResp.Meta.TotalPages > 1 {\n\t\tg, _ := errgroup.WithContext(ctx)\n\t\tg.SetLimit(8)\n\n\t\tfor i := 2; i <= firstResp.Meta.TotalPages; i++ {\n\t\t\tpage := i\n\t\t\tg.Go(func() error {\n\t\t\t\tvar resp ListResp\n\t\t\t\terr := d.request(http.MethodGet, \"/api/files\", func(req *resty.Request) {\n\t\t\t\t\treq.SetQueryParams(map[string]string{\n\t\t\t\t\t\t\"path\":  dir.GetPath(),\n\t\t\t\t\t\t\"limit\": \"500\",\n\t\t\t\t\t\t\"page\":  strconv.Itoa(page),\n\t\t\t\t\t})\n\t\t\t\t}, &resp)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tpagesData[page-1] = resp.Items\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\tif err := g.Wait(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar allItems []Object\n\tfor _, items := range pagesData {\n\t\tallItems = append(allItems, items...)\n\t}\n\n\treturn utils.SliceConvert(allItems, func(src Object) (model.Obj, error) {\n\t\treturn &model.Object{\n\t\t\tPath: path.Join(dir.GetPath(), src.Name),\n\t\t\tID:   src.ID,\n\t\t\tName: src.Name,\n\t\t\tSize: func() int64 {\n\t\t\t\tif src.Type == \"folder\" {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn src.Size\n\t\t\t}(),\n\t\t\tIsFolder: src.Type == \"folder\",\n\t\t\tModified: src.UpdatedAt,\n\t\t}, nil\n\t})\n}\n\nfunc (d *Teldrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.UseShareLink {\n\t\tshareObj, err := d.getShareFileById(file.GetID())\n\t\tif err != nil || shareObj == nil {\n\t\t\tif err := d.createShareFile(file.GetID()); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tshareObj, err = d.getShareFileById(file.GetID())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn &model.Link{\n\t\t\tURL: d.Address + \"/api/shares/\" + url.PathEscape(shareObj.Id) + \"/files/\" + url.PathEscape(file.GetID()) + \"/\" + url.PathEscape(file.GetName()),\n\t\t}, nil\n\t}\n\treturn &model.Link{\n\t\tURL: d.Address + \"/api/files/\" + url.PathEscape(file.GetID()) + \"/\" + url.PathEscape(file.GetName()),\n\t\tHeader: http.Header{\n\t\t\t\"Cookie\": {d.Cookie},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Teldrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.request(http.MethodPost, \"/api/files/mkdir\", func(req *resty.Request) {\n\t\treq.SetBody(map[string]interface{}{\n\t\t\t\"path\": parentDir.GetPath() + \"/\" + dirName,\n\t\t})\n\t}, nil)\n}\n\nfunc (d *Teldrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tbody := base.Json{\n\t\t\"ids\":               []string{srcObj.GetID()},\n\t\t\"destinationParent\": dstDir.GetID(),\n\t}\n\treturn d.request(http.MethodPost, \"/api/files/move\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Teldrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tbody := base.Json{\n\t\t\"name\": newName,\n\t}\n\treturn d.request(http.MethodPatch, \"/api/files/{id}\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", srcObj.GetID())\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Teldrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tcopyConcurrentLimit := 4\n\tcopyManager := NewCopyManager(ctx, copyConcurrentLimit, d)\n\tcopyManager.startWorkers()\n\tcopyManager.G.Go(func() error {\n\t\tdefer close(copyManager.TaskChan)\n\t\treturn copyManager.generateTasks(ctx, srcObj, dstDir)\n\t})\n\treturn copyManager.G.Wait()\n}\n\nfunc (d *Teldrive) Remove(ctx context.Context, obj model.Obj) error {\n\tbody := base.Json{\n\t\t\"ids\": []string{obj.GetID()},\n\t}\n\treturn d.request(http.MethodPost, \"/api/files/delete\", func(req *resty.Request) {\n\t\treq.SetBody(body)\n\t}, nil)\n}\n\nfunc (d *Teldrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tfileId := uuid.New().String()\n\tchunkSizeInMB := d.ChunkSize\n\tchunkSize := chunkSizeInMB * 1024 * 1024 // Convert MB to bytes\n\ttotalSize := file.GetSize()\n\ttotalParts := int(math.Ceil(float64(totalSize) / float64(chunkSize)))\n\tmaxRetried := 3\n\n\t// delete the upload task when finished or failed\n\tdefer func() {\n\t\t_ = d.request(http.MethodDelete, \"/api/uploads/{id}\", func(req *resty.Request) {\n\t\t\treq.SetPathParam(\"id\", fileId)\n\t\t}, nil)\n\t}()\n\n\tif obj, err := d.getFile(dstDir.GetPath(), file.GetName(), file.IsDir()); err == nil {\n\t\tif err = d.Remove(ctx, obj); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// start the upload process\n\tif err := d.request(http.MethodGet, \"/api/uploads/fileId\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", fileId)\n\t}, nil); err != nil {\n\t\treturn err\n\t}\n\tif totalSize == 0 {\n\t\treturn d.touch(file.GetName(), dstDir.GetPath())\n\t}\n\n\tif totalParts <= 1 {\n\t\treturn d.doSingleUpload(ctx, dstDir, file, up, maxRetried, totalParts, chunkSize, fileId)\n\t}\n\n\treturn d.doMultiUpload(ctx, dstDir, file, up, maxRetried, totalParts, chunkSize, fileId)\n}\n\nfunc (d *Teldrive) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Teldrive) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Teldrive) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Teldrive) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n\t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n\t// return errs.NotImplement to use an internal archive tool\n\treturn nil, errs.NotImplement\n}\n\n//func (d *Teldrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Teldrive)(nil)\n"
  },
  {
    "path": "drivers/teldrive/meta.go",
    "content": "package teldrive\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress           string `json:\"url\" required:\"true\"`\n\tCookie            string `json:\"cookie\" type:\"string\" required:\"true\" help:\"access_token=xxx\"`\n\tUseShareLink      bool   `json:\"use_share_link\" type:\"bool\" default:\"false\" help:\"Create share link when getting link to support 302. If disabled, you need to enable web proxy.\"`\n\tChunkSize         int64  `json:\"chunk_size\" type:\"number\" default:\"10\" help:\"Chunk size in MiB\"`\n\tRandomChunkName   bool   `json:\"random_chunk_name\" type:\"bool\" default:\"true\" help:\"Random chunk name\"`\n\tUploadConcurrency int64  `json:\"upload_concurrency\" type:\"number\" default:\"4\" help:\"Concurrency upload requests\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Teldrive\",\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Teldrive{}\n\t})\n}\n"
  },
  {
    "path": "drivers/teldrive/types.go",
    "content": "package teldrive\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sync/semaphore\"\n)\n\ntype ErrResp struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Object struct {\n\tID        string    `json:\"id\"`\n\tName      string    `json:\"name\"`\n\tType      string    `json:\"type\"`\n\tMimeType  string    `json:\"mimeType\"`\n\tCategory  string    `json:\"category,omitempty\"`\n\tParentId  string    `json:\"parentId\"`\n\tSize      int64     `json:\"size\"`\n\tEncrypted bool      `json:\"encrypted\"`\n\tUpdatedAt time.Time `json:\"updatedAt\"`\n}\n\ntype ListResp struct {\n\tItems []Object `json:\"items\"`\n\tMeta  struct {\n\t\tCount       int `json:\"count\"`\n\t\tTotalPages  int `json:\"totalPages\"`\n\t\tCurrentPage int `json:\"currentPage\"`\n\t} `json:\"meta\"`\n}\n\ntype FilePart struct {\n\tName      string `json:\"name\"`\n\tPartId    int    `json:\"partId\"`\n\tPartNo    int    `json:\"partNo\"`\n\tChannelId int    `json:\"channelId\"`\n\tSize      int    `json:\"size\"`\n\tEncrypted bool   `json:\"encrypted\"`\n\tSalt      string `json:\"salt\"`\n}\n\ntype chunkTask struct {\n\tchunkIdx  int\n\tfileName  string\n\tchunkSize int64\n\treader    io.ReadSeeker\n\tss        stream.StreamSectionReaderIF\n}\n\ntype CopyManager struct {\n\tTaskChan chan CopyTask\n\tSem      *semaphore.Weighted\n\tG        *errgroup.Group\n\tCtx      context.Context\n\td        *Teldrive\n}\n\ntype CopyTask struct {\n\tSrcObj model.Obj\n\tDstDir model.Obj\n}\n\ntype ShareObj struct {\n\tId        string    `json:\"id\"`\n\tProtected bool      `json:\"protected\"`\n\tUserId    int       `json:\"userId\"`\n\tType      string    `json:\"type\"`\n\tName      string    `json:\"name\"`\n\tExpiresAt time.Time `json:\"expiresAt\"`\n}\n"
  },
  {
    "path": "drivers/teldrive/upload.go",
    "content": "package teldrive\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/net/context\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sync/semaphore\"\n)\n\n// create empty file\nfunc (d *Teldrive) touch(name, path string) error {\n\tuploadBody := base.Json{\n\t\t\"name\": name,\n\t\t\"type\": \"file\",\n\t\t\"path\": path,\n\t}\n\tif err := d.request(http.MethodPost, \"/api/files\", func(req *resty.Request) {\n\t\treq.SetBody(uploadBody)\n\t}, nil); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc getMD5Hash(text string) string {\n\thash := md5.Sum([]byte(text))\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc (d *Teldrive) createFileOnUploadSuccess(name, id, path string, uploadedFileParts []FilePart, totalSize int64) error {\n\tremoteFileParts, err := d.getFilePart(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// check if the uploaded file parts match the remote file parts\n\tif len(remoteFileParts) != len(uploadedFileParts) {\n\t\treturn fmt.Errorf(\"[Teldrive] file parts count mismatch: expected %d, got %d\", len(uploadedFileParts), len(remoteFileParts))\n\t}\n\tformatParts := make([]base.Json, 0)\n\tfor _, p := range remoteFileParts {\n\t\tformatParts = append(formatParts, base.Json{\n\t\t\t\"id\":   p.PartId,\n\t\t\t\"salt\": p.Salt,\n\t\t})\n\t}\n\tuploadBody := base.Json{\n\t\t\"name\":  name,\n\t\t\"type\":  \"file\",\n\t\t\"path\":  path,\n\t\t\"parts\": formatParts,\n\t\t\"size\":  totalSize,\n\t}\n\t// create file here\n\tif err := d.request(http.MethodPost, \"/api/files\", func(req *resty.Request) {\n\t\treq.SetBody(uploadBody)\n\t}, nil); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Teldrive) checkFilePartExist(fileId string, partId int) (FilePart, error) {\n\tvar uploadedParts []FilePart\n\tvar filePart FilePart\n\n\tif err := d.request(http.MethodGet, \"/api/uploads/{id}\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", fileId)\n\t}, &uploadedParts); err != nil {\n\t\treturn filePart, err\n\t}\n\n\tfor _, part := range uploadedParts {\n\t\tif part.PartId == partId {\n\t\t\treturn part, nil\n\t\t}\n\t}\n\n\treturn filePart, nil\n}\n\nfunc (d *Teldrive) getFilePart(fileId string) ([]FilePart, error) {\n\tvar uploadedParts []FilePart\n\tif err := d.request(http.MethodGet, \"/api/uploads/{id}\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", fileId)\n\t}, &uploadedParts); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn uploadedParts, nil\n}\n\nfunc (d *Teldrive) singleUploadRequest(ctx context.Context, fileId string, callback base.ReqCallback, resp any) error {\n\turl := d.Address + \"/api/uploads/\" + fileId\n\tclient := resty.New().SetTimeout(0)\n\n\treq := client.R().\n\t\tSetContext(ctx)\n\treq.SetHeader(\"Cookie\", d.Cookie)\n\treq.SetHeader(\"Content-Type\", \"application/octet-stream\")\n\treq.SetContentLength(true)\n\treq.AddRetryCondition(func(r *resty.Response, err error) bool {\n\t\treturn false\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\t_req, err := req.Execute(http.MethodPost, url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _req.IsError() {\n\t\treturn &e\n\t}\n\treturn nil\n}\n\nfunc (d *Teldrive) doSingleUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up model.UpdateProgress,\n\tmaxRetried, totalParts int, chunkSize int64, fileId string) error {\n\n\ttotalSize := file.GetSize()\n\tvar fileParts []FilePart\n\tvar uploaded int64 = 0\n\tvar partName string\n\tchunkSize = min(totalSize, chunkSize)\n\tss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\tchunkCnt := 0\n\tfor uploaded < totalSize {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tcurChunkSize := min(totalSize-uploaded, chunkSize)\n\t\trd, err := ss.GetSectionReader(uploaded, curChunkSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tchunkCnt += 1\n\t\tfilePart := &FilePart{}\n\t\tif err := retry.Do(func() error {\n\n\t\t\tif _, err := rd.Seek(0, io.SeekStart); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif d.RandomChunkName {\n\t\t\t\tpartName = getMD5Hash(uuid.New().String())\n\t\t\t} else {\n\t\t\t\tpartName = file.GetName()\n\t\t\t\tif totalParts > 1 {\n\t\t\t\t\tpartName = fmt.Sprintf(\"%s.part.%03d\", file.GetName(), chunkCnt)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err := d.singleUploadRequest(ctx, fileId, func(req *resty.Request) {\n\t\t\t\tuploadParams := map[string]string{\n\t\t\t\t\t\"partName\": partName,\n\t\t\t\t\t\"partNo\":   strconv.Itoa(chunkCnt),\n\t\t\t\t\t\"fileName\": file.GetName(),\n\t\t\t\t}\n\t\t\t\treq.SetQueryParams(uploadParams)\n\t\t\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, rd))\n\t\t\t\treq.SetHeader(\"Content-Length\", strconv.FormatInt(curChunkSize, 10))\n\t\t\t}, filePart); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\t\tretry.Context(ctx),\n\t\t\tretry.Attempts(uint(maxRetried)),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second)); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif filePart.Name != \"\" {\n\t\t\tfileParts = append(fileParts, *filePart)\n\t\t\tuploaded += curChunkSize\n\t\t\tup(float64(uploaded) / float64(totalSize) * 100)\n\t\t\tss.FreeSectionReader(rd)\n\t\t} else {\n\t\t\t// For common situation this code won't reach\n\t\t\treturn fmt.Errorf(\"[Teldrive] upload chunk %d failed: filePart Somehow missing\", chunkCnt)\n\t\t}\n\n\t}\n\n\treturn d.createFileOnUploadSuccess(file.GetName(), fileId, dstDir.GetPath(), fileParts, totalSize)\n}\n\nfunc (d *Teldrive) doMultiUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up model.UpdateProgress,\n\tmaxRetried, totalParts int, chunkSize int64, fileId string) error {\n\n\tconcurrent := d.UploadConcurrency\n\tg, ctx := errgroup.WithContext(ctx)\n\tsem := semaphore.NewWeighted(int64(concurrent))\n\tchunkChan := make(chan chunkTask, concurrent*2)\n\tresultChan := make(chan FilePart, concurrent)\n\ttotalSize := file.GetSize()\n\n\tss, err := stream.NewStreamSectionReader(file, int(totalSize), &up)\n\tif err != nil {\n\t\treturn err\n\t}\n\tssLock := sync.Mutex{}\n\tg.Go(func() error {\n\t\tdefer close(chunkChan)\n\n\t\tchunkIdx := 0\n\t\tfor chunkIdx < totalParts {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\toffset := int64(chunkIdx) * chunkSize\n\t\t\tcurChunkSize := min(totalSize-offset, chunkSize)\n\n\t\t\tssLock.Lock()\n\t\t\treader, err := ss.GetSectionReader(offset, curChunkSize)\n\t\t\tssLock.Unlock()\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttask := chunkTask{\n\t\t\t\tchunkIdx:  chunkIdx + 1,\n\t\t\t\tchunkSize: curChunkSize,\n\t\t\t\tfileName:  file.GetName(),\n\t\t\t\treader:    reader,\n\t\t\t\tss:        ss,\n\t\t\t}\n\t\t\t// freeSectionReader will be called in d.uploadSingleChunk\n\t\t\tselect {\n\t\t\tcase chunkChan <- task:\n\t\t\t\tchunkIdx++\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tfor i := 0; i < int(concurrent); i++ {\n\t\tg.Go(func() error {\n\t\t\tfor task := range chunkChan {\n\t\t\t\tif err := sem.Acquire(ctx, 1); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfilePart, err := d.uploadSingleChunk(ctx, fileId, task, totalParts, maxRetried)\n\t\t\t\tsem.Release(1)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"upload chunk %d failed: %w\", task.chunkIdx, err)\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase resultChan <- *filePart:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn ctx.Err()\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\tvar fileParts []FilePart\n\tvar collectErr error\n\tcollectDone := make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(collectDone)\n\t\tfileParts = make([]FilePart, 0, totalParts)\n\n\t\tdone := make(chan error, 1)\n\t\tgo func() {\n\t\t\tdone <- g.Wait()\n\t\t\tclose(resultChan)\n\t\t}()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase filePart, ok := <-resultChan:\n\t\t\t\tif !ok {\n\t\t\t\t\tcollectErr = <-done\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfileParts = append(fileParts, filePart)\n\t\t\tcase err := <-done:\n\t\t\t\tcollectErr = err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-collectDone\n\n\tif collectErr != nil {\n\t\treturn fmt.Errorf(\"multi-upload failed: %w\", collectErr)\n\t}\n\tsort.Slice(fileParts, func(i, j int) bool {\n\t\treturn fileParts[i].PartNo < fileParts[j].PartNo\n\t})\n\n\treturn d.createFileOnUploadSuccess(file.GetName(), fileId, dstDir.GetPath(), fileParts, totalSize)\n}\n\nfunc (d *Teldrive) uploadSingleChunk(ctx context.Context, fileId string, task chunkTask, totalParts, maxRetried int) (*FilePart, error) {\n\tfilePart := &FilePart{}\n\tretryCount := 0\n\tvar partName string\n\tdefer task.ss.FreeSectionReader(task.reader)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tif existingPart, err := d.checkFilePartExist(fileId, task.chunkIdx); err == nil && existingPart.Name != \"\" {\n\t\t\treturn &existingPart, nil\n\t\t}\n\n\t\tif _, err := task.reader.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif d.RandomChunkName {\n\t\t\tpartName = getMD5Hash(uuid.New().String())\n\t\t} else {\n\t\t\tpartName = task.fileName\n\t\t\tif totalParts > 1 {\n\t\t\t\tpartName = fmt.Sprintf(\"%s.part.%03d\", task.fileName, task.chunkIdx)\n\t\t\t}\n\t\t}\n\n\t\terr := d.singleUploadRequest(ctx, fileId, func(req *resty.Request) {\n\t\t\tuploadParams := map[string]string{\n\t\t\t\t\"partName\": partName,\n\t\t\t\t\"partNo\":   strconv.Itoa(task.chunkIdx),\n\t\t\t\t\"fileName\": task.fileName,\n\t\t\t}\n\t\t\treq.SetQueryParams(uploadParams)\n\t\t\treq.SetBody(driver.NewLimitedUploadStream(ctx, task.reader))\n\t\t\treq.SetHeader(\"Content-Length\", strconv.Itoa(int(task.chunkSize)))\n\t\t}, filePart)\n\n\t\tif err == nil {\n\t\t\treturn filePart, nil\n\t\t}\n\n\t\tif retryCount >= maxRetried {\n\t\t\treturn nil, fmt.Errorf(\"upload failed after %d retries: %w\", maxRetried, err)\n\t\t}\n\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {\n\t\t\tcontinue\n\t\t}\n\n\t\tretryCount++\n\t\tutils.Log.Errorf(\"[Teldrive] upload error: %v, retrying %d times\", err, retryCount)\n\n\t\tbackoffDuration := time.Duration(retryCount*retryCount) * time.Second\n\t\tif backoffDuration > 30*time.Second {\n\t\t\tbackoffDuration = 30 * time.Second\n\t\t}\n\n\t\tselect {\n\t\tcase <-time.After(backoffDuration):\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/teldrive/util.go",
    "content": "package teldrive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *Teldrive) request(method string, pathname string, callback base.ReqCallback, resp interface{}) error {\n\turl := d.Address + pathname\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Cookie\", d.Cookie)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\t_req, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _req.IsError() {\n\t\treturn &e\n\t}\n\treturn nil\n}\n\nfunc (d *Teldrive) getFile(path, name string, isFolder bool) (model.Obj, error) {\n\tresp := &ListResp{}\n\terr := d.request(http.MethodGet, \"/api/files\", func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"path\": path,\n\t\t\t\"name\": name,\n\t\t\t\"type\": func() string {\n\t\t\t\tif isFolder {\n\t\t\t\t\treturn \"folder\"\n\t\t\t\t}\n\t\t\t\treturn \"file\"\n\t\t\t}(),\n\t\t\t\"operation\": \"find\",\n\t\t})\n\t}, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resp.Items) == 0 {\n\t\treturn nil, fmt.Errorf(\"file not found: %s/%s\", path, name)\n\t}\n\tobj := resp.Items[0]\n\treturn &model.Object{\n\t\tID:       obj.ID,\n\t\tName:     obj.Name,\n\t\tSize:     obj.Size,\n\t\tIsFolder: obj.Type == \"folder\",\n\t}, err\n}\n\nfunc (err *ErrResp) Error() string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"[Teldrive] message:%s Error code:%d\", err.Message, err.Code)\n}\n\nfunc (d *Teldrive) createShareFile(fileId string) error {\n\tvar errResp ErrResp\n\tif err := d.request(http.MethodPost, \"/api/files/{id}/share\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", fileId)\n\t\treq.SetBody(base.Json{\n\t\t\t\"expiresAt\": getDateTime(),\n\t\t})\n\t}, &errResp); err != nil {\n\t\treturn err\n\t}\n\n\tif errResp.Message != \"\" {\n\t\treturn &errResp\n\t}\n\n\treturn nil\n}\n\nfunc (d *Teldrive) getShareFileById(fileId string) (*ShareObj, error) {\n\tvar shareObj ShareObj\n\tif err := d.request(http.MethodGet, \"/api/files/{id}/share\", func(req *resty.Request) {\n\t\treq.SetPathParam(\"id\", fileId)\n\t}, &shareObj); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &shareObj, nil\n}\n\nfunc getDateTime() string {\n\tnow := time.Now().UTC()\n\tformattedWithMs := now.Add(time.Hour * 1).Format(\"2006-01-02T15:04:05.000Z\")\n\treturn formattedWithMs\n}\n"
  },
  {
    "path": "drivers/template/driver.go",
    "content": "package template\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Template struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Template) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Template) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Template) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Template) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Template) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\t// TODO return the files list, required\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\t// TODO return link of file, required\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\t// TODO create folder, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO move obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\t// TODO rename obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO copy obj, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *Template) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// TODO upload file, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\t// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\t// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\t// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\t// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional\n\t// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir\n\t// return errs.NotImplement to use an internal archive tool\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Template) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\t// TODO return storage details (total space, free space, etc.)\n\treturn nil, errs.NotImplement\n}\n\n//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Template)(nil)\n"
  },
  {
    "path": "drivers/template/meta.go",
    "content": "package template\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\tdriver.RootID\n\t// define other\n\tField string `json:\"field\" type:\"select\" required:\"true\" options:\"a,b,c\" default:\"a\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Template\",\n\tLocalSort:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"root, / or other\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n\tNoLinkURL:         false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Template{}\n\t})\n}\n"
  },
  {
    "path": "drivers/template/types.go",
    "content": "package template\n"
  },
  {
    "path": "drivers/template/util.go",
    "content": "package template\n\n// do others that not defined in Driver interface\n"
  },
  {
    "path": "drivers/terabox/driver.go",
    "content": "package terabox\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Terabox struct {\n\tmodel.Storage\n\tAddition\n\tJsToken           string\n\turl_domain_prefix string\n\tbase_url          string\n}\n\nfunc (d *Terabox) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Terabox) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Terabox) Init(ctx context.Context) error {\n\tvar resp CheckLoginResp\n\td.base_url = \"https://www.terabox.com\"\n\td.url_domain_prefix = \"jp\"\n\t_, err := d.get(\"/api/check/login\", nil, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Errno != 0 {\n\t\tif resp.Errno == 9000 {\n\t\t\treturn fmt.Errorf(\"terabox is not yet available in this area\")\n\t\t}\n\t\treturn fmt.Errorf(\"failed to check login status according to cookie\")\n\t}\n\treturn err\n}\n\nfunc (d *Terabox) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Terabox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\tobj := fileToObj(src)\n\t\tobj.Path = stdpath.Join(dir.GetPath(), obj.Name)\n\t\treturn obj, nil\n\t})\n}\n\nfunc (d *Terabox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.DownloadAPI == \"crack\" {\n\t\treturn d.linkCrack(file, args)\n\t}\n\treturn d.linkOfficial(file, args)\n}\n\nfunc (d *Terabox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tparams := map[string]string{\n\t\t\"a\": \"commit\",\n\t}\n\tdata := map[string]string{\n\t\t\"path\":       stdpath.Join(parentDir.GetPath(), dirName),\n\t\t\"isdir\":      \"1\",\n\t\t\"block_list\": \"[]\",\n\t}\n\tres, err := d.post_form(\"/api/create\", params, data, nil)\n\tlog.Debugln(string(res))\n\treturn err\n}\n\nfunc (d *Terabox) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"dest\":    dstDir.GetPath(),\n\t\t\t\"newname\": srcObj.GetName(),\n\t\t},\n\t}\n\t_, err := d.manage(\"move\", data)\n\treturn err\n}\n\nfunc (d *Terabox) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"newname\": newName,\n\t\t},\n\t}\n\t_, err := d.manage(\"rename\", data)\n\treturn err\n}\n\nfunc (d *Terabox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdata := []base.Json{\n\t\t{\n\t\t\t\"path\":    srcObj.GetPath(),\n\t\t\t\"dest\":    dstDir.GetPath(),\n\t\t\t\"newname\": srcObj.GetName(),\n\t\t},\n\t}\n\t_, err := d.manage(\"copy\", data)\n\treturn err\n}\n\nfunc (d *Terabox) Remove(ctx context.Context, obj model.Obj) error {\n\tdata := []string{obj.GetPath()}\n\t_, err := d.manage(\"delete\", data)\n\treturn err\n}\n\nfunc (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tresp, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tGet(\"https://\" + d.url_domain_prefix + \"-data.terabox.com/rest/2.0/pcs/file?method=locateupload\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar locateupload_resp LocateUploadResp\n\terr = utils.Json.Unmarshal(resp.Body(), &locateupload_resp)\n\tif err != nil {\n\t\tlog.Debugln(resp)\n\t\treturn err\n\t}\n\tlog.Debugln(locateupload_resp)\n\n\t// precreate file\n\trawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())\n\tpath := encodeURIComponent(rawPath)\n\n\tvar precreateBlockListStr string\n\tif stream.GetSize() > initialChunkSize {\n\t\tprecreateBlockListStr = `[\"5910a591dd8fc18c32a8f3df4fdc1761\",\"a5fc157d78e6ad1c7e114b056c92821e\"]`\n\t} else {\n\t\tprecreateBlockListStr = `[\"5910a591dd8fc18c32a8f3df4fdc1761\"]`\n\t}\n\n\tdata := map[string]string{\n\t\t\"path\":                  rawPath,\n\t\t\"autoinit\":              \"1\",\n\t\t\"target_path\":           dstDir.GetPath(),\n\t\t\"block_list\":            precreateBlockListStr,\n\t\t\"local_mtime\":           strconv.FormatInt(stream.ModTime().Unix(), 10),\n\t\t\"file_limit_switch_v34\": \"true\",\n\t}\n\tvar precreateResp PrecreateResp\n\tlog.Debugln(data)\n\tres, err := d.post_form(\"/api/precreate\", nil, data, &precreateResp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"%+v\", precreateResp)\n\tif precreateResp.Errno != 0 {\n\t\tlog.Debugln(string(res))\n\t\treturn fmt.Errorf(\"[terabox] failed to precreate file, errno: %d\", precreateResp.Errno)\n\t}\n\tif precreateResp.ReturnType == 2 {\n\t\treturn nil\n\t}\n\n\t// upload chunks\n\ttempFile, err := stream.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := map[string]string{\n\t\t\"method\":   \"upload\",\n\t\t\"path\":     path,\n\t\t\"uploadid\": precreateResp.Uploadid,\n\t}\n\n\tstreamSize := stream.GetSize()\n\tchunkSize := calculateChunkSize(streamSize)\n\tchunkByteData := make([]byte, chunkSize)\n\tcount := int((streamSize + chunkSize - 1) / chunkSize)\n\tleft := streamSize\n\tuploadBlockList := make([]string, 0, count)\n\th := md5.New()\n\n\tfor partseq := 0; partseq < count; partseq++ {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tbyteSize := chunkSize\n\t\tvar byteData []byte\n\t\tif left >= chunkSize {\n\t\t\tbyteData = chunkByteData\n\t\t} else {\n\t\t\tbyteSize = left\n\t\t\tbyteData = make([]byte, byteSize)\n\t\t}\n\t\tleft -= byteSize\n\t\t_, err = io.ReadFull(tempFile, byteData)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// calculate md5\n\t\th.Write(byteData)\n\t\tlocalMD5 := hex.EncodeToString(h.Sum(nil))\n\t\tuploadBlockList = append(uploadBlockList, localMD5)\n\t\th.Reset()\n\n\t\tu := \"https://\" + locateupload_resp.Host + \"/rest/2.0/pcs/superfile2\"\n\t\tparams[\"partseq\"] = strconv.Itoa(partseq)\n\t\tlog.Debugf(\"%+v\", params)\n\n\t\terr = retry.Do(\n\t\t\tfunc() error {\n\t\t\t\tfileReader := driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData))\n\t\t\t\tres, err := d.post_multipart(u, params, \"file\", stream.GetName(), fileReader, nil)\n\t\t\t\tlog.Debugln(string(res))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trspmd5 := utils.Json.Get(res, \"md5\").ToString()\n\t\t\t\tif localMD5 != rspmd5 {\n\t\t\t\t\tlog.Debugf(\"MD5 mismatch, our MD5: %s, server: %s\", localMD5, rspmd5)\n\t\t\t\t\treturn fmt.Errorf(\"MD5 mismatch\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tretry.Attempts(5),\n\t\t\tretry.DelayType(retry.FixedDelay),\n\t\t\tretry.Context(ctx),\n\t\t)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif count > 0 {\n\t\t\tup(float64(partseq) * 100 / float64(count))\n\t\t}\n\t}\n\n\t// create file\n\tparams = map[string]string{\n\t\t\"isdir\": \"0\",\n\t\t\"rtype\": \"1\",\n\t}\n\n\tuploadBlockListStr, err := utils.Json.MarshalToString(uploadBlockList)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = map[string]string{\n\t\t\"path\":        rawPath,\n\t\t\"size\":        strconv.FormatInt(stream.GetSize(), 10),\n\t\t\"uploadid\":    precreateResp.Uploadid,\n\t\t\"target_path\": dstDir.GetPath(),\n\t\t\"block_list\":  uploadBlockListStr,\n\t\t\"local_mtime\": strconv.FormatInt(stream.ModTime().Unix(), 10),\n\t}\n\tvar createResp CreateResp\n\tres, err = d.post_form(\"/api/create\", params, data, &createResp)\n\tlog.Debugln(string(res))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif createResp.Errno != 0 {\n\t\treturn fmt.Errorf(\"[terabox] failed to create file, errno: %d\", createResp.Errno)\n\t}\n\treturn nil\n}\n\nvar _ driver.Driver = (*Terabox)(nil)\n"
  },
  {
    "path": "drivers/terabox/meta.go",
    "content": "package terabox\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tCookie string `json:\"cookie\" required:\"true\"`\n\t//JsToken        string `json:\"js_token\" type:\"string\" required:\"true\"`\n\tDownloadAPI    string `json:\"download_api\" type:\"select\" options:\"official,crack\" default:\"official\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"name,time,size\" default:\"name\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Terabox\",\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Terabox{}\n\t})\n}\n"
  },
  {
    "path": "drivers/terabox/types.go",
    "content": "package terabox\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype File struct {\n\t//TkbindId     int    `json:\"tkbind_id\"`\n\t//OwnerType    int    `json:\"owner_type\"`\n\t//Category     int    `json:\"category\"`\n\t//RealCategory string `json:\"real_category\"`\n\tFsId        int64 `json:\"fs_id\"`\n\tServerMtime int64 `json:\"server_mtime\"`\n\t//OperId      int   `json:\"oper_id\"`\n\t//ServerCtime int   `json:\"server_ctime\"`\n\tThumbs struct {\n\t\t//Icon string `json:\"icon\"`\n\t\tUrl3 string `json:\"url3\"`\n\t\t//Url2 string `json:\"url2\"`\n\t\t//Url1 string `json:\"url1\"`\n\t} `json:\"thumbs\"`\n\t//Wpfile         int    `json:\"wpfile\"`\n\t//LocalMtime     int    `json:\"local_mtime\"`\n\tSize int64 `json:\"size\"`\n\t//ExtentTinyint7 int    `json:\"extent_tinyint7\"`\n\tPath string `json:\"path\"`\n\t//Share          int    `json:\"share\"`\n\t//ServerAtime    int    `json:\"server_atime\"`\n\t//Pl             int    `json:\"pl\"`\n\t//LocalCtime     int    `json:\"local_ctime\"`\n\tServerFilename string `json:\"server_filename\"`\n\t//Md5            string `json:\"md5\"`\n\t//OwnerId        int    `json:\"owner_id\"`\n\t//Unlist int `json:\"unlist\"`\n\tIsdir int `json:\"isdir\"`\n}\n\ntype ListResp struct {\n\tErrno    int    `json:\"errno\"`\n\tGuidInfo string `json:\"guid_info\"`\n\tList     []File `json:\"list\"`\n\t//RequestId int64  `json:\"request_id\"` 接口返回有时是int有时是string\n\tGuid int `json:\"guid\"`\n}\n\nfunc fileToObj(f File) *model.ObjThumb {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       strconv.FormatInt(f.FsId, 10),\n\t\t\tName:     f.ServerFilename,\n\t\t\tSize:     f.Size,\n\t\t\tModified: time.Unix(f.ServerMtime, 0),\n\t\t\tIsFolder: f.Isdir == 1,\n\t\t},\n\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},\n\t}\n}\n\ntype DownloadResp struct {\n\tErrno int `json:\"errno\"`\n\tDlink []struct {\n\t\tDlink string `json:\"dlink\"`\n\t} `json:\"dlink\"`\n}\n\ntype DownloadResp2 struct {\n\tErrno int `json:\"errno\"`\n\tInfo  []struct {\n\t\tDlink string `json:\"dlink\"`\n\t} `json:\"info\"`\n\t//RequestID int64 `json:\"request_id\"`\n}\n\ntype HomeInfoResp struct {\n\tErrno int `json:\"errno\"`\n\tData  struct {\n\t\tSign1     string `json:\"sign1\"`\n\t\tSign3     string `json:\"sign3\"`\n\t\tTimestamp int    `json:\"timestamp\"`\n\t} `json:\"data\"`\n}\n\ntype PrecreateResp struct {\n\tPath       string `json:\"path\"`\n\tUploadid   string `json:\"uploadid\"`\n\tReturnType int    `json:\"return_type\"`\n\tBlockList  []int  `json:\"block_list\"`\n\tErrno      int    `json:\"errno\"`\n\t//RequestId  int64  `json:\"request_id\"`\n}\n\ntype CheckLoginResp struct {\n\tErrno int `json:\"errno\"`\n}\n\ntype LocateUploadResp struct {\n\tHost string `json:\"host\"`\n}\n\ntype CreateResp struct {\n\tErrno int `json:\"errno\"`\n}\n"
  },
  {
    "path": "drivers/terabox/util.go",
    "content": "package terabox\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tinitialChunkSize     int64 = 4 << 20 // 4MB\n\tinitialSizeThreshold int64 = 4 << 30 // 4GB\n)\n\nfunc getStrBetween(raw, start, end string) string {\n\tregexPattern := fmt.Sprintf(`%s(.*?)%s`, regexp.QuoteMeta(start), regexp.QuoteMeta(end))\n\tregex := regexp.MustCompile(regexPattern)\n\tmatches := regex.FindStringSubmatch(raw)\n\tif len(matches) < 2 {\n\t\treturn \"\"\n\t}\n\tmid := matches[1]\n\treturn mid\n}\n\nfunc (d *Terabox) resetJsToken() error {\n\tu := d.base_url\n\tres, err := base.RestyClient.R().SetHeaders(map[string]string{\n\t\t\"Cookie\":           d.Cookie,\n\t\t\"Accept\":           \"application/json, text/plain, */*\",\n\t\t\"Referer\":          d.base_url,\n\t\t\"User-Agent\":       \"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox\",\n\t\t\"X-Requested-With\": \"XMLHttpRequest\",\n\t}).Get(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\thtml := res.String()\n\tjsToken := getStrBetween(html, \"`function%20fn%28a%29%7Bwindow.jsToken%20%3D%20a%7D%3Bfn%28%22\", \"%22%29`\")\n\tif jsToken == \"\" {\n\t\treturn fmt.Errorf(\"jsToken not found, html: %s\", html)\n\t}\n\td.JsToken = jsToken\n\treturn nil\n}\n\nfunc (d *Terabox) request(rurl string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":           d.Cookie,\n\t\t\"Accept\":           \"application/json, text/plain, */*\",\n\t\t\"Referer\":          d.base_url,\n\t\t\"User-Agent\":       \"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox\",\n\t\t\"X-Requested-With\": \"XMLHttpRequest\",\n\t})\n\treq.SetQueryParams(map[string]string{\n\t\t\"app_id\":     \"250528\",\n\t\t\"web\":        \"1\",\n\t\t\"channel\":    \"dubox\",\n\t\t\"clienttype\": \"0\",\n\t\t\"jsToken\":    d.JsToken,\n\t})\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\tfull_url := d.base_url + rurl\n\tif strings.HasPrefix(rurl, \"https://\") {\n\t\tfull_url = rurl\n\t}\n\n\tres, err := req.Execute(method, full_url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terrno := utils.Json.Get(res.Body(), \"errno\").ToInt()\n\tif errno == 4000023 || errno == 450016 {\n\t\t// reget jsToken\n\t\terr = d.resetJsToken()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !utils.IsBool(noRetry...) {\n\t\t\treturn d.request(rurl, method, callback, resp, true)\n\t\t}\n\t} else if errno == -6 {\n\t\theader := res.Header()\n\t\tlog.Debugln(header)\n\t\turlDomainPrefix := header.Get(\"Url-Domain-Prefix\")\n\t\tif len(urlDomainPrefix) > 0 {\n\t\t\td.url_domain_prefix = urlDomainPrefix\n\t\t\td.base_url = \"https://\" + d.url_domain_prefix + \".terabox.com\"\n\t\t\tlog.Debugln(\"Redirect base_url to\", d.base_url)\n\t\t\treturn d.request(rurl, method, callback, resp, noRetry...)\n\t\t}\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Terabox) get(pathname string, params map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.request(pathname, http.MethodGet, func(req *resty.Request) {\n\t\tif params != nil {\n\t\t\treq.SetQueryParams(params)\n\t\t}\n\t}, resp)\n}\n\nfunc (d *Terabox) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) {\n\treturn d.request(pathname, http.MethodPost, func(req *resty.Request) {\n\t\tif params != nil {\n\t\t\treq.SetQueryParams(params)\n\t\t}\n\t\treq.SetBody(data)\n\t}, resp)\n}\n\nfunc (d *Terabox) post_form(pathname string, params map[string]string, data map[string]string, resp interface{}) ([]byte, error) {\n\treturn d.request(pathname, http.MethodPost, func(req *resty.Request) {\n\t\tif params != nil {\n\t\t\treq.SetQueryParams(params)\n\t\t}\n\t\treq.SetFormData(data)\n\t}, resp)\n}\n\nfunc (d *Terabox) post_multipart(\n\tpathname string,\n\tparams map[string]string,\n\tfileFieldName string,\n\tfileName string,\n\tfileReader io.Reader,\n\tresp interface{},\n) ([]byte, error) {\n\treturn d.request(pathname, http.MethodPost, func(req *resty.Request) {\n\t\tif params != nil {\n\t\t\treq.SetQueryParams(params)\n\t\t}\n\t\treq.SetFileReader(fileFieldName, fileName, fileReader)\n\t}, resp)\n}\n\nfunc (d *Terabox) getFiles(dir string) ([]File, error) {\n\tpage := 1\n\tnum := 100\n\tparams := map[string]string{\n\t\t\"dir\": dir,\n\t}\n\tif d.OrderBy != \"\" {\n\t\tparams[\"order\"] = d.OrderBy\n\t\tif d.OrderDirection == \"desc\" {\n\t\t\tparams[\"desc\"] = \"1\"\n\t\t}\n\t}\n\tres := make([]File, 0)\n\tfor {\n\t\tparams[\"page\"] = strconv.Itoa(page)\n\t\tparams[\"num\"] = strconv.Itoa(num)\n\t\tvar resp ListResp\n\t\t_, err := d.get(\"/api/list\", params, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resp.Errno == 9000 {\n\t\t\treturn nil, fmt.Errorf(\"terabox is not yet available in this area\")\n\t\t}\n\t\tif len(resp.List) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tres = append(res, resp.List...)\n\t\tpage++\n\t}\n\treturn res, nil\n}\n\nfunc sign(s1, s2 string) string {\n\tvar a = make([]int, 256)\n\tvar p = make([]int, 256)\n\tvar o []byte\n\tvar v = len(s1)\n\tfor q := 0; q < 256; q++ {\n\t\ta[q] = int(s1[(q % v) : (q%v)+1][0])\n\t\tp[q] = q\n\t}\n\tfor u, q := 0, 0; q < 256; q++ {\n\t\tu = (u + p[q] + a[q]) % 256\n\t\tp[q], p[u] = p[u], p[q]\n\t}\n\tfor i, u, q := 0, 0, 0; q < len(s2); q++ {\n\t\ti = (i + 1) % 256\n\t\tu = (u + p[i]) % 256\n\t\tp[i], p[u] = p[u], p[i]\n\t\tk := p[((p[i] + p[u]) % 256)]\n\t\to = append(o, byte(int(s2[q])^k))\n\t}\n\treturn base64.StdEncoding.EncodeToString(o)\n}\n\nfunc (d *Terabox) genSign() (string, error) {\n\tvar resp HomeInfoResp\n\t_, err := d.get(\"/api/home/info\", map[string]string{}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn sign(resp.Data.Sign3, resp.Data.Sign1), nil\n}\n\nfunc (d *Terabox) linkOfficial(file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp DownloadResp\n\tsignString, err := d.genSign()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tparams := map[string]string{\n\t\t\"type\":      \"dlink\",\n\t\t\"fidlist\":   fmt.Sprintf(\"[%s]\", file.GetID()),\n\t\t\"sign\":      signString,\n\t\t\"vip\":       \"2\",\n\t\t\"timestamp\": strconv.FormatInt(time.Now().Unix(), 10),\n\t}\n\t_, err = d.get(\"/api/download\", params, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(resp.Dlink) == 0 {\n\t\treturn nil, fmt.Errorf(\"fid %s no dlink found, errno: %d\", file.GetID(), resp.Errno)\n\t}\n\n\tres, err := base.NoRedirectClient.R().SetHeader(\"Cookie\", d.Cookie).SetHeader(\"User-Agent\", \"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox\").Get(resp.Dlink[0].Dlink)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu := res.Header().Get(\"location\")\n\treturn &model.Link{\n\t\tURL: u,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{\"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox\"},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Terabox) linkCrack(file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp DownloadResp2\n\tparam := map[string]string{\n\t\t\"target\": fmt.Sprintf(\"[\\\"%s\\\"]\", file.GetPath()),\n\t\t\"dlink\":  \"1\",\n\t\t\"origin\": \"dlna\",\n\t}\n\t_, err := d.get(\"/api/filemetas\", param, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: resp.Info[0].Dlink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": []string{\"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox\"},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Terabox) manage(opera string, filelist interface{}) ([]byte, error) {\n\tparams := map[string]string{\n\t\t\"onnest\": \"fail\",\n\t\t\"opera\":  opera,\n\t}\n\tmarshal, err := utils.Json.Marshal(filelist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata := fmt.Sprintf(\"async=0&filelist=%s&ondup=newcopy\", encodeURIComponent(string(marshal)))\n\treturn d.post(\"/api/filemanager\", params, data, nil)\n}\n\nfunc encodeURIComponent(str string) string {\n\tr := url.QueryEscape(str)\n\tr = strings.ReplaceAll(r, \"+\", \"%20\")\n\treturn r\n}\n\nfunc calculateChunkSize(streamSize int64) int64 {\n\tchunkSize := initialChunkSize\n\tsizeThreshold := initialSizeThreshold\n\n\tif streamSize < chunkSize {\n\t\treturn streamSize\n\t}\n\n\tfor streamSize > sizeThreshold {\n\t\tchunkSize <<= 1\n\t\tsizeThreshold <<= 1\n\t}\n\n\treturn chunkSize\n}\n"
  },
  {
    "path": "drivers/thunder/driver.go",
    "content": "package thunder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Thunder struct {\n\t*XunLeiCommon\n\tmodel.Storage\n\tAddition\n\n\tidentity string\n}\n\nfunc (x *Thunder) Config() driver.Config {\n\treturn config\n}\n\nfunc (x *Thunder) GetAddition() driver.Additional {\n\treturn &x.Addition\n}\n\nfunc (x *Thunder) Init(ctx context.Context) (err error) {\n\t// 初始化所需参数\n\tif x.XunLeiCommon == nil {\n\t\tx.XunLeiCommon = &XunLeiCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient: base.NewRestyClient(),\n\t\t\t\tAlgorithms: []string{\n\t\t\t\t\t\"9uJNVj/wLmdwKrJaVj/omlQ\",\n\t\t\t\t\t\"Oz64Lp0GigmChHMf/6TNfxx7O9PyopcczMsnf\",\n\t\t\t\t\t\"Eb+L7Ce+Ej48u\",\n\t\t\t\t\t\"jKY0\",\n\t\t\t\t\t\"ASr0zCl6v8W4aidjPK5KHd1Lq3t+vBFf41dqv5+fnOd\",\n\t\t\t\t\t\"wQlozdg6r1qxh0eRmt3QgNXOvSZO6q/GXK\",\n\t\t\t\t\t\"gmirk+ciAvIgA/cxUUCema47jr/YToixTT+Q6O\",\n\t\t\t\t\t\"5IiCoM9B1/788ntB\",\n\t\t\t\t\t\"P07JH0h6qoM6TSUAK2aL9T5s2QBVeY9JWvalf\",\n\t\t\t\t\t\"+oK0AN\",\n\t\t\t\t},\n\t\t\t\tDeviceID: func() string {\n\t\t\t\t\tif len(x.DeviceID) != 32 {\n\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.Username + x.Password)\n\t\t\t\t\t}\n\t\t\t\t\treturn x.DeviceID\n\t\t\t\t}(),\n\t\t\t\tClientID:          \"Xp6vsxz_7IYVw2BB\",\n\t\t\t\tClientSecret:      \"Xp6vsy4tN9toTVdMSpomVdXpRmES\",\n\t\t\t\tClientVersion:     \"8.31.0.9726\",\n\t\t\t\tPackageName:       \"com.xunlei.downloadprovider\",\n\t\t\t\tUserAgent:         \"ANDROID-com.xunlei.downloadprovider/8.31.0.9726 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/512000 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)\",\n\t\t\t\tDownloadUserAgent: \"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)\",\n\t\t\t\tSpace:             x.Space,\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t\trefreshTokenFunc: func() error {\n\t\t\t\t// 通过RefreshToken刷新\n\t\t\t\ttoken, err := x.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// 重新登录\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t\t}\n\t\t\t\t\t// 清空 信任密钥\n\t\t\t\t\tx.Addition.CreditKey = \"\"\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\treturn err\n\t\t\t},\n\t\t}\n\t}\n\n\t// 自定义验证码token\n\tctoekn := strings.TrimSpace(x.CaptchaToken)\n\tif ctoekn != \"\" {\n\t\tx.SetCaptchaToken(ctoekn)\n\t}\n\n\tif x.Addition.CreditKey != \"\" {\n\t\tx.SetCreditKey(x.Addition.CreditKey)\n\t}\n\n\tif x.Addition.DeviceID != \"\" {\n\t\tx.Common.DeviceID = x.Addition.DeviceID\n\t} else {\n\t\tx.Addition.DeviceID = x.Common.DeviceID\n\t\top.MustSaveDriverStorage(x)\n\t}\n\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif x.identity != identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\t// 登录\n\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 清空 信任密钥\n\t\tx.Addition.CreditKey = \"\"\n\t\tx.SetTokenResp(token)\n\t}\n\treturn nil\n}\n\nfunc (x *Thunder) Drop(ctx context.Context) error {\n\treturn nil\n}\n\ntype ThunderExpert struct {\n\t*XunLeiCommon\n\tmodel.Storage\n\tExpertAddition\n\n\tidentity string\n}\n\nfunc (x *ThunderExpert) Config() driver.Config {\n\treturn configExpert\n}\n\nfunc (x *ThunderExpert) GetAddition() driver.Additional {\n\treturn &x.ExpertAddition\n}\n\nfunc (x *ThunderExpert) Init(ctx context.Context) (err error) {\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif identity != x.identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\tx.XunLeiCommon = &XunLeiCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient: base.NewRestyClient(),\n\n\t\t\t\tDeviceID: func() string {\n\t\t\t\t\tif len(x.DeviceID) != 32 {\n\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.DeviceID)\n\t\t\t\t\t}\n\t\t\t\t\treturn x.DeviceID\n\t\t\t\t}(),\n\t\t\t\tClientID:          x.ClientID,\n\t\t\t\tClientSecret:      x.ClientSecret,\n\t\t\t\tClientVersion:     x.ClientVersion,\n\t\t\t\tPackageName:       x.PackageName,\n\t\t\t\tUserAgent:         x.UserAgent,\n\t\t\t\tDownloadUserAgent: x.DownloadUserAgent,\n\t\t\t\tUseVideoUrl:       x.UseVideoUrl,\n\t\t\t\tSpace:             x.Space,\n\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tif x.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.CaptchaToken)\n\t\t}\n\n\t\tif x.ExpertAddition.CreditKey != \"\" {\n\t\t\tx.SetCreditKey(x.ExpertAddition.CreditKey)\n\t\t}\n\n\t\tif x.ExpertAddition.DeviceID != \"\" {\n\t\t\tx.Common.DeviceID = x.ExpertAddition.DeviceID\n\t\t} else {\n\t\t\tx.ExpertAddition.DeviceID = x.Common.DeviceID\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\n\t\t// 签名方法\n\t\tif x.SignType == \"captcha_sign\" {\n\t\t\tx.Common.Timestamp = x.Timestamp\n\t\t\tx.Common.CaptchaSign = x.CaptchaSign\n\t\t} else {\n\t\t\tx.Common.Algorithms = strings.Split(x.Algorithms, \",\")\n\t\t}\n\n\t\t// 登录方式\n\t\tif x.LoginType == \"refresh_token\" {\n\t\t\t// 通过RefreshToken登录\n\t\t\ttoken, err := x.XunLeiCommon.RefreshToken(x.ExpertAddition.RefreshToken)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tx.SetTokenResp(token)\n\n\t\t\t// 刷新token方法\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\t\t} else {\n\t\t\t// 通过用户密码登录\n\t\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 清空 信任密钥\n\t\t\tx.ExpertAddition.CreditKey = \"\"\n\t\t\tx.SetTokenResp(token)\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t}\n\t\t\t\t\t// 清空 信任密钥\n\t\t\t\t\tx.ExpertAddition.CreditKey = \"\"\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\t\t}\n\t} else {\n\t\t// 仅修改验证码token\n\t\tif x.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.CaptchaToken)\n\t\t}\n\t\tx.XunLeiCommon.UserAgent = x.UserAgent\n\t\tx.XunLeiCommon.DownloadUserAgent = x.DownloadUserAgent\n\t\tx.XunLeiCommon.UseVideoUrl = x.UseVideoUrl\n\t}\n\treturn nil\n}\n\nfunc (x *ThunderExpert) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (x *ThunderExpert) SetTokenResp(token *TokenResp) {\n\tx.XunLeiCommon.SetTokenResp(token)\n\tif token != nil {\n\t\tx.ExpertAddition.RefreshToken = token.RefreshToken\n\t}\n}\n\ntype XunLeiCommon struct {\n\t*Common\n\t*TokenResp     // 登录信息\n\t*CoreLoginResp // core登录信息\n\n\trefreshTokenFunc func() error\n}\n\nfunc (xc *XunLeiCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn xc.getFiles(ctx, dir.GetID())\n}\n\nfunc (xc *XunLeiCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar lFile Files\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", file.GetID())\n\t\tr.SetQueryParam(\"space\", xc.Space)\n\t}, &lFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := &model.Link{\n\t\tURL: lFile.WebContentLink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": {xc.DownloadUserAgent},\n\t\t},\n\t}\n\n\tif xc.UseVideoUrl {\n\t\tfor _, media := range lFile.Medias {\n\t\t\tif media.Link.URL != \"\" {\n\t\t\t\tlink.URL = media.Link.URL\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t\tstrs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)\n\t\tif len(strs) == 2 {\n\t\t\ttimestamp, err := strconv.ParseInt(strs[1], 10, 64)\n\t\t\tif err == nil {\n\t\t\t\texpired := time.Duration(timestamp-time.Now().Unix()) * time.Second\n\t\t\t\tlink.Expiration = &expired\n\t\t\t}\n\t\t}\n\t*/\n\treturn link, nil\n}\n\nfunc (xc *XunLeiCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"kind\":      FOLDER,\n\t\t\t\"name\":      dirName,\n\t\t\t\"parent_id\": parentDir.GetID(),\n\t\t\t\"space\":     xc.Space,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\":batchMove\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"to\":    base.Json{\"parent_id\": dstDir.GetID()},\n\t\t\t\"ids\":   []string{srcObj.GetID()},\n\t\t\t\"space\": xc.Space,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodPatch, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", srcObj.GetID())\n\t\tr.SetBody(&base.Json{\n\t\t\t\"name\":  newName,\n\t\t\t\"space\": xc.Space,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\":batchCopy\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"to\":    base.Json{\"parent_id\": dstDir.GetID()},\n\t\t\t\"ids\":   []string{srcObj.GetID()},\n\t\t\t\"space\": xc.Space,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiCommon) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}/trash\", http.MethodPatch, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", obj.GetID())\n\t\tr.SetQueryParam(\"space\", xc.Space)\n\t\tr.SetBody(\"{}\")\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tgcid := file.GetHash().GetHash(hash_extend.GCID)\n\tvar err error\n\tif len(gcid) < hash_extend.GCID.Width {\n\t\t_, gcid, err = stream.CacheFullAndHash(file, &up, hash_extend.GCID, file.GetSize())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar resp UploadTaskResponse\n\t_, err = xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"kind\":        FILE,\n\t\t\t\"parent_id\":   dstDir.GetID(),\n\t\t\t\"name\":        file.GetName(),\n\t\t\t\"size\":        file.GetSize(),\n\t\t\t\"hash\":        gcid,\n\t\t\t\"upload_type\": UPLOAD_TYPE_RESUMABLE,\n\t\t\t\"space\":       xc.Space,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparam := resp.Resumable.Params\n\tif resp.UploadType == UPLOAD_TYPE_RESUMABLE {\n\t\tparam.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+\".\")\n\t\ts, err := session.NewSession(&aws.Config{\n\t\t\tCredentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),\n\t\t\tRegion:      aws.String(\"xunlei\"),\n\t\t\tEndpoint:    aws.String(param.Endpoint),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuploader := s3manager.NewUploader(s)\n\t\tif file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\t\tuploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)\n\t\t}\n\t\t_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{\n\t\t\tBucket:  aws.String(param.Bucket),\n\t\t\tKey:     aws.String(param.Key),\n\t\t\tExpires: aws.Time(param.Expiration),\n\t\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\t\tReader:         file,\n\t\t\t\tUpdateProgress: up,\n\t\t\t}),\n\t\t})\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (xc *XunLeiCommon) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tvar about AboutResponse\n\t_, err := xc.Request(API_URL+\"/about\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t}, &about)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttotal, err := strconv.ParseInt(about.Quota.Limit, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(about.Quota.Usage, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nfunc (xc *XunLeiCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tvar pageToken string\n\tfor {\n\t\tvar fileList FileList\n\t\t_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"space\":      xc.Space,\n\t\t\t\t\"__type\":     \"drive\",\n\t\t\t\t\"refresh\":    \"true\",\n\t\t\t\t\"__sync\":     \"true\",\n\t\t\t\t\"parent_id\":  folderId,\n\t\t\t\t\"page_token\": pageToken,\n\t\t\t\t\"with_audit\": \"true\",\n\t\t\t\t\"limit\":      \"100\",\n\t\t\t\t\"filters\":    `{\"phase\":{\"eq\":\"PHASE_TYPE_COMPLETE\"},\"trashed\":{\"eq\":false}}`,\n\t\t\t})\n\t\t\t// 获取硬盘挂载目录等\n\t\t\tif xc.Space != \"\" {\n\t\t\t\tr.SetQueryParamsFromValues(url.Values{\n\t\t\t\t\t\"with\": []string{\n\t\t\t\t\t\t\"withCategoryDiskMountPath\",\n\t\t\t\t\t\t\"withCategoryDriveCachePath\",\n\t\t\t\t\t\t\"withCategoryHistoryDownloadPath\",\n\t\t\t\t\t\t\"withReadOnlyFS\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}, &fileList)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := 0; i < len(fileList.Files); i++ {\n\t\t\tfiles = append(files, &fileList.Files[i])\n\t\t}\n\n\t\tif fileList.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tpageToken = fileList.NextPageToken\n\t}\n\treturn files, nil\n}\n\n// 设置刷新Token的方法\nfunc (xc *XunLeiCommon) SetRefreshTokenFunc(fn func() error) {\n\txc.refreshTokenFunc = fn\n}\n\n// 设置Token\nfunc (xc *XunLeiCommon) SetTokenResp(tr *TokenResp) {\n\txc.TokenResp = tr\n}\n\nfunc (xc *XunLeiCommon) SetCoreTokenResp(tr *CoreLoginResp) {\n\txc.CoreLoginResp = tr\n}\n\n// 携带Authorization和CaptchaToken的请求\nfunc (xc *XunLeiCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tdata, err := xc.Common.Request(url, method, func(req *resty.Request) {\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Authorization\":   xc.Token(),\n\t\t\t\"X-Captcha-Token\": xc.GetCaptchaToken(),\n\t\t})\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t}, resp)\n\n\terrResp, ok := err.(*ErrResp)\n\tif !ok {\n\t\treturn nil, err\n\t}\n\n\tswitch errResp.ErrorCode {\n\tcase 0:\n\t\treturn data, nil\n\tcase 4122, 4121, 10, 16:\n\t\tif xc.refreshTokenFunc != nil {\n\t\t\tif err = xc.refreshTokenFunc(); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\tcase 9: // 验证码token过期\n\t\tif err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.TokenResp.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, err\n\t}\n\treturn xc.Request(url, method, callback, resp)\n}\n\n// 刷新Token\nfunc (xc *XunLeiCommon) RefreshToken(refreshToken string) (*TokenResp, error) {\n\tvar resp TokenResp\n\t_, err := xc.Common.Request(XLUSER_API_URL+\"/auth/token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(&base.Json{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": refreshToken,\n\t\t\t\"client_id\":     xc.ClientID,\n\t\t\t\"client_secret\": xc.ClientSecret,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.RefreshToken == \"\" {\n\t\treturn nil, errs.EmptyToken\n\t}\n\treturn &resp, nil\n}\n\n// 登录\nfunc (xc *XunLeiCommon) Login(username, password string) (*TokenResp, error) {\n\t//v3 login拿到 sessionID\n\tsessionID, err := xc.CoreLogin(username, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//v1 login拿到令牌\n\turl := XLUSER_API_URL + \"/auth/signin/token\"\n\tif err = xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp TokenResp\n\t_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetPathParam(\"client_id\", xc.ClientID)\n\t\treq.SetBody(&SignInRequest{\n\t\t\tClientID:     xc.ClientID,\n\t\t\tClientSecret: xc.ClientSecret,\n\t\t\tProvider:     SignProvider,\n\t\t\tSigninToken:  sessionID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (xc *XunLeiCommon) IsLogin() bool {\n\tif xc.TokenResp == nil {\n\t\treturn false\n\t}\n\t_, err := xc.Request(XLUSER_API_URL+\"/user/me\", http.MethodGet, nil, nil)\n\treturn err == nil\n}\n\n// 离线下载文件\nfunc (xc *XunLeiCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {\n\tvar resp OfflineDownloadResp\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"kind\":        FILE,\n\t\t\t\"name\":        fileName,\n\t\t\t\"parent_id\":   parentDir.GetID(),\n\t\t\t\"upload_type\": UPLOAD_TYPE_URL,\n\t\t\t\"space\":       xc.Space,\n\t\t\t\"url\": base.Json{\n\t\t\t\t\"url\": fileUrl,\n\t\t\t},\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Task, err\n}\n\n/*\n获取离线下载任务列表\n*/\nfunc (xc *XunLeiCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {\n\tres := make([]OfflineTask, 0)\n\n\tvar resp OfflineListResp\n\t_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"type\":       \"offline\",\n\t\t\t\t\"limit\":      \"10000\",\n\t\t\t\t\"page_token\": nextPageToken,\n\t\t\t\t\"space\":      xc.Space,\n\t\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get offline list: %w\", err)\n\t}\n\tres = append(res, resp.Tasks...)\n\treturn res, nil\n}\n\nfunc (xc *XunLeiCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {\n\t_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"task_ids\":     strings.Join(taskIDs, \",\"),\n\t\t\t\t\"delete_files\": strconv.FormatBool(deleteFiles),\n\t\t\t\t\"space\":        xc.Space,\n\t\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete tasks %v: %w\", taskIDs, err)\n\t}\n\treturn nil\n}\n\nfunc (xc *XunLeiCommon) CoreLogin(username string, password string) (sessionID string, err error) {\n\turl := XLUSER_API_BASE_URL + \"/xluser.core.login/v3/login\"\n\tvar resp CoreLoginResp\n\tres, err := xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"User-Agent\", \"android-ok-http-client/xl-acc-sdk/version-5.0.12.512000\")\n\t\treq.SetBody(&CoreLoginRequest{\n\t\t\tProtocolVersion: \"301\",\n\t\t\tSequenceNo:      \"1000012\",\n\t\t\tPlatformVersion: \"10\",\n\t\t\tIsCompressed:    \"0\",\n\t\t\tAppid:           APPID,\n\t\t\tClientVersion:   \"8.31.0.9726\",\n\t\t\tPeerID:          \"00000000000000000000000000000000\",\n\t\t\tAppName:         \"ANDROID-com.xunlei.downloadprovider\",\n\t\t\tSdkVersion:      \"512000\",\n\t\t\tDevicesign:      generateDeviceSign(xc.DeviceID, xc.PackageName),\n\t\t\tNetWorkType:     \"WIFI\",\n\t\t\tProviderName:    \"NONE\",\n\t\t\tDeviceModel:     \"M2004J7AC\",\n\t\t\tDeviceName:      \"Xiaomi_M2004j7ac\",\n\t\t\tOSVersion:       \"12\",\n\t\t\tCreditkey:       xc.GetCreditKey(),\n\t\t\tHl:              \"zh-CN\",\n\t\t\tUserName:        username,\n\t\t\tPassWord:        password,\n\t\t\tVerifyKey:       \"\",\n\t\t\tVerifyCode:      \"\",\n\t\t\tIsMd5Pwd:        \"0\",\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = utils.Json.Unmarshal(res, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\txc.SetCoreTokenResp(&resp)\n\n\tsessionID = resp.SessionID\n\n\treturn sessionID, nil\n}\n"
  },
  {
    "path": "drivers/thunder/meta.go",
    "content": "package thunder\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// 高级设置\ntype ExpertAddition struct {\n\tdriver.RootID\n\n\tLoginType string `json:\"login_type\" type:\"select\" options:\"user,refresh_token\" default:\"user\"`\n\tSignType  string `json:\"sign_type\" type:\"select\" options:\"algorithms,captcha_sign\" default:\"algorithms\"`\n\n\t// 登录方式1\n\tUsername string `json:\"username\" required:\"true\" help:\"login type is user,this is required\"`\n\tPassword string `json:\"password\" required:\"true\" help:\"login type is user,this is required\"`\n\t// 登录方式2\n\tRefreshToken string `json:\"refresh_token\" required:\"true\" help:\"login type is refresh_token,this is required\"`\n\n\t// 签名方法1\n\tAlgorithms string `json:\"algorithms\" required:\"true\" help:\"sign type is algorithms,this is required\" default:\"9uJNVj/wLmdwKrJaVj/omlQ,Oz64Lp0GigmChHMf/6TNfxx7O9PyopcczMsnf,Eb+L7Ce+Ej48u,jKY0,ASr0zCl6v8W4aidjPK5KHd1Lq3t+vBFf41dqv5+fnOd,wQlozdg6r1qxh0eRmt3QgNXOvSZO6q/GXK,gmirk+ciAvIgA/cxUUCema47jr/YToixTT+Q6O,5IiCoM9B1/788ntB,P07JH0h6qoM6TSUAK2aL9T5s2QBVeY9JWvalf,+oK0AN\"`\n\t// 签名方法2\n\tCaptchaSign string `json:\"captcha_sign\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\tTimestamp   string `json:\"timestamp\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\n\t// 验证码\n\tCaptchaToken string `json:\"captcha_token\"`\n\t// 信任密钥\n\tCreditKey string `json:\"credit_key\" help:\"credit key,used for login\"`\n\n\t// 必要且影响登录,由签名决定\n\tDeviceID      string `json:\"device_id\" default:\"\"`\n\tClientID      string `json:\"client_id\"  required:\"true\" default:\"Xp6vsxz_7IYVw2BB\"`\n\tClientSecret  string `json:\"client_secret\"  required:\"true\" default:\"Xp6vsy4tN9toTVdMSpomVdXpRmES\"`\n\tClientVersion string `json:\"client_version\"  required:\"true\" default:\"8.31.0.9726\"`\n\tPackageName   string `json:\"package_name\"  required:\"true\" default:\"com.xunlei.downloadprovider\"`\n\n\t//不影响登录,影响下载速度\n\tUserAgent         string `json:\"user_agent\"  required:\"true\" default:\"ANDROID-com.xunlei.downloadprovider/8.31.0.9726 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/512000 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)\"`\n\tDownloadUserAgent string `json:\"download_user_agent\"  required:\"true\" default:\"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)\"`\n\n\t//优先使用视频链接代替下载链接\n\tUseVideoUrl bool `json:\"use_video_url\"`\n\n\tSpace string `json:\"space\" default:\"\" help:\"device id for remote device\"`\n}\n\n// 登录特征,用于判断是否重新登录\nfunc (i *ExpertAddition) GetIdentity() string {\n\thash := md5.New()\n\tif i.LoginType == \"refresh_token\" {\n\t\thash.Write([]byte(i.RefreshToken))\n\t} else {\n\t\thash.Write([]byte(i.Username + i.Password))\n\t}\n\n\tif i.SignType == \"captcha_sign\" {\n\t\thash.Write([]byte(i.CaptchaSign + i.Timestamp))\n\t} else {\n\t\thash.Write([]byte(i.Algorithms))\n\t}\n\n\thash.Write([]byte(i.DeviceID))\n\thash.Write([]byte(i.ClientID))\n\thash.Write([]byte(i.ClientSecret))\n\thash.Write([]byte(i.ClientVersion))\n\thash.Write([]byte(i.PackageName))\n\treturn hex.EncodeToString(hash.Sum(nil))\n}\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername     string `json:\"username\" required:\"true\"`\n\tPassword     string `json:\"password\" required:\"true\"`\n\tCaptchaToken string `json:\"captcha_token\"`\n\t// 信任密钥\n\tCreditKey string `json:\"credit_key\" help:\"credit key,used for login\"`\n\t// 登录设备ID\n\tDeviceID string `json:\"device_id\" default:\"\"`\n\n\tSpace string `json:\"space\" default:\"\" help:\"device id for remote device\"`\n}\n\n// 登录特征,用于判断是否重新登录\nfunc (i *Addition) GetIdentity() string {\n\treturn utils.GetMD5EncodeStr(i.Username + i.Password)\n}\n\nvar config = driver.Config{\n\tName:      \"Thunder\",\n\tLocalSort: true,\n}\n\nvar configExpert = driver.Config{\n\tName:      \"ThunderExpert\",\n\tLocalSort: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Thunder{}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ThunderExpert{}\n\t})\n}\n"
  },
  {
    "path": "drivers/thunder/types.go",
    "content": "package thunder\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n)\n\ntype ErrResp struct {\n\tErrorCode        int64  `json:\"error_code\"`\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n\t//\tErrorDetails   interface{} `json:\"error_details\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\tif e.ErrorMsg == \"success\" {\n\t\treturn false\n\t}\n\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ErrorDescription != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ErrorDescription: %s \", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)\n}\n\n/*\n* 验证码Token\n**/\ntype CaptchaTokenRequest struct {\n\tAction       string            `json:\"action\"`\n\tCaptchaToken string            `json:\"captcha_token\"`\n\tClientID     string            `json:\"client_id\"`\n\tDeviceID     string            `json:\"device_id\"`\n\tMeta         map[string]string `json:\"meta\"`\n\tRedirectUri  string            `json:\"redirect_uri\"`\n}\n\ntype CaptchaTokenResponse struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tUrl          string `json:\"url\"`\n}\n\n/*\n* 登录\n**/\ntype TokenResp struct {\n\tTokenType    string `json:\"token_type\"`\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\n\tSub    string `json:\"sub\"`\n\tUserID string `json:\"user_id\"`\n}\n\nfunc (t *TokenResp) Token() string {\n\treturn fmt.Sprint(t.TokenType, \" \", t.AccessToken)\n}\n\ntype SignInRequest struct {\n\tClientID     string `json:\"client_id\"`\n\tClientSecret string `json:\"client_secret\"`\n\n\tProvider    string `json:\"provider\"`\n\tSigninToken string `json:\"signin_token\"`\n}\n\ntype CoreLoginRequest struct {\n\tProtocolVersion string `json:\"protocolVersion\"`\n\tSequenceNo      string `json:\"sequenceNo\"`\n\tPlatformVersion string `json:\"platformVersion\"`\n\tIsCompressed    string `json:\"isCompressed\"`\n\tAppid           string `json:\"appid\"`\n\tClientVersion   string `json:\"clientVersion\"`\n\tPeerID          string `json:\"peerID\"`\n\tAppName         string `json:\"appName\"`\n\tSdkVersion      string `json:\"sdkVersion\"`\n\tDevicesign      string `json:\"devicesign\"`\n\tNetWorkType     string `json:\"netWorkType\"`\n\tProviderName    string `json:\"providerName\"`\n\tDeviceModel     string `json:\"deviceModel\"`\n\tDeviceName      string `json:\"deviceName\"`\n\tOSVersion       string `json:\"OSVersion\"`\n\tCreditkey       string `json:\"creditkey\"`\n\tHl              string `json:\"hl\"`\n\tUserName        string `json:\"userName\"`\n\tPassWord        string `json:\"passWord\"`\n\tVerifyKey       string `json:\"verifyKey\"`\n\tVerifyCode      string `json:\"verifyCode\"`\n\tIsMd5Pwd        string `json:\"isMd5Pwd\"`\n}\n\ntype CoreLoginResp struct {\n\tAccount   string `json:\"account\"`\n\tCreditkey string `json:\"creditkey\"`\n\t/*\tError              string `json:\"error\"`\n\t\tErrorCode          string `json:\"errorCode\"`\n\t\tErrorDescription   string `json:\"error_description\"`*/\n\tExpiresIn          int    `json:\"expires_in\"`\n\tIsCompressed       string `json:\"isCompressed\"`\n\tIsSetPassWord      string `json:\"isSetPassWord\"`\n\tKeepAliveMinPeriod string `json:\"keepAliveMinPeriod\"`\n\tKeepAlivePeriod    string `json:\"keepAlivePeriod\"`\n\tLoginKey           string `json:\"loginKey\"`\n\tNickName           string `json:\"nickName\"`\n\tPlatformVersion    string `json:\"platformVersion\"`\n\tProtocolVersion    string `json:\"protocolVersion\"`\n\tSecureKey          string `json:\"secureKey\"`\n\tSequenceNo         string `json:\"sequenceNo\"`\n\tSessionID          string `json:\"sessionID\"`\n\tTimestamp          string `json:\"timestamp\"`\n\tUserID             string `json:\"userID\"`\n\tUserName           string `json:\"userName\"`\n\tUserNewNo          string `json:\"userNewNo\"`\n\tVersion            string `json:\"version\"`\n\t/*\tVipList []struct {\n\t\tExpireDate string `json:\"expireDate\"`\n\t\tIsAutoDeduct string `json:\"isAutoDeduct\"`\n\t\tIsVip string `json:\"isVip\"`\n\t\tIsYear string `json:\"isYear\"`\n\t\tPayID string `json:\"payId\"`\n\t\tPayName string `json:\"payName\"`\n\t\tRegister string `json:\"register\"`\n\t\tVasid string `json:\"vasid\"`\n\t\tVasType string `json:\"vasType\"`\n\t\tVipDayGrow string `json:\"vipDayGrow\"`\n\t\tVipGrow string `json:\"vipGrow\"`\n\t\tVipLevel string `json:\"vipLevel\"`\n\t\tIcon struct {\n\t\t\tGeneral string `json:\"general\"`\n\t\t\tSmall string `json:\"small\"`\n\t\t} `json:\"icon\"`\n\t} `json:\"vipList\"`*/\n}\n\n/*\n* 文件\n**/\ntype FileList struct {\n\tKind            string  `json:\"kind\"`\n\tNextPageToken   string  `json:\"next_page_token\"`\n\tFiles           []Files `json:\"files\"`\n\tVersion         string  `json:\"version\"`\n\tVersionOutdated bool    `json:\"version_outdated\"`\n}\n\ntype Link struct {\n\tURL    string    `json:\"url\"`\n\tToken  string    `json:\"token\"`\n\tExpire time.Time `json:\"expire\"`\n\tType   string    `json:\"type\"`\n}\n\nvar _ model.Obj = (*Files)(nil)\n\ntype Files struct {\n\tKind     string `json:\"kind\"`\n\tID       string `json:\"id\"`\n\tParentID string `json:\"parent_id\"`\n\tName     string `json:\"name\"`\n\t//UserID         string    `json:\"user_id\"`\n\tSize string `json:\"size\"`\n\t//Revision       string    `json:\"revision\"`\n\t//FileExtension  string    `json:\"file_extension\"`\n\t//MimeType       string    `json:\"mime_type\"`\n\t//Starred        bool      `json:\"starred\"`\n\tWebContentLink string    `json:\"web_content_link\"`\n\tCreatedTime    time.Time `json:\"created_time\"`\n\tModifiedTime   time.Time `json:\"modified_time\"`\n\tIconLink       string    `json:\"icon_link\"`\n\tThumbnailLink  string    `json:\"thumbnail_link\"`\n\t// Md5Checksum    string    `json:\"md5_checksum\"`\n\tHash string `json:\"hash\"`\n\t// Links map[string]Link `json:\"links\"`\n\t// Phase string          `json:\"phase\"`\n\t// Audit struct {\n\t// \tStatus  string `json:\"status\"`\n\t// \tMessage string `json:\"message\"`\n\t// \tTitle   string `json:\"title\"`\n\t// } `json:\"audit\"`\n\tMedias []struct {\n\t\t//Category       string `json:\"category\"`\n\t\t//IconLink       string `json:\"icon_link\"`\n\t\t//IsDefault      bool   `json:\"is_default\"`\n\t\t//IsOrigin       bool   `json:\"is_origin\"`\n\t\t//IsVisible      bool   `json:\"is_visible\"`\n\t\tLink Link `json:\"link\"`\n\t\t//MediaID        string `json:\"media_id\"`\n\t\t//MediaName      string `json:\"media_name\"`\n\t\t//NeedMoreQuota  bool   `json:\"need_more_quota\"`\n\t\t//Priority       int    `json:\"priority\"`\n\t\t//RedirectLink   string `json:\"redirect_link\"`\n\t\t//ResolutionName string `json:\"resolution_name\"`\n\t\t// Video          struct {\n\t\t// \tAudioCodec string `json:\"audio_codec\"`\n\t\t// \tBitRate    int    `json:\"bit_rate\"`\n\t\t// \tDuration   int    `json:\"duration\"`\n\t\t// \tFrameRate  int    `json:\"frame_rate\"`\n\t\t// \tHeight     int    `json:\"height\"`\n\t\t// \tVideoCodec string `json:\"video_codec\"`\n\t\t// \tVideoType  string `json:\"video_type\"`\n\t\t// \tWidth      int    `json:\"width\"`\n\t\t// } `json:\"video\"`\n\t\t// VipTypes []string `json:\"vip_types\"`\n\t} `json:\"medias\"`\n\tTrashed     bool   `json:\"trashed\"`\n\tDeleteTime  string `json:\"delete_time\"`\n\tOriginalURL string `json:\"original_url\"`\n\t//Params            struct{} `json:\"params\"`\n\t//OriginalFileIndex int    `json:\"original_file_index\"`\n\t//Space             string `json:\"space\"`\n\t//Apps              []interface{} `json:\"apps\"`\n\t//Writable   bool   `json:\"writable\"`\n\t//FolderType string `json:\"folder_type\"`\n\t//Collection interface{} `json:\"collection\"`\n}\n\nfunc (c *Files) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(hash_extend.GCID, c.Hash)\n}\n\nfunc (c *Files) GetSize() int64        { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }\nfunc (c *Files) GetName() string       { return c.Name }\nfunc (c *Files) CreateTime() time.Time { return c.CreatedTime }\nfunc (c *Files) ModTime() time.Time    { return c.ModifiedTime }\nfunc (c *Files) IsDir() bool           { return c.Kind == FOLDER }\nfunc (c *Files) GetID() string         { return c.ID }\nfunc (c *Files) GetPath() string       { return \"\" }\nfunc (c *Files) Thumb() string         { return c.ThumbnailLink }\n\n/*\n* 上传\n**/\ntype UploadTaskResponse struct {\n\tUploadType string `json:\"upload_type\"`\n\n\t/*//UPLOAD_TYPE_FORM\n\tForm struct {\n\t\t//Headers struct{} `json:\"headers\"`\n\t\tKind       string `json:\"kind\"`\n\t\tMethod     string `json:\"method\"`\n\t\tMultiParts struct {\n\t\t\tOSSAccessKeyID string `json:\"OSSAccessKeyId\"`\n\t\t\tSignature      string `json:\"Signature\"`\n\t\t\tCallback       string `json:\"callback\"`\n\t\t\tKey            string `json:\"key\"`\n\t\t\tPolicy         string `json:\"policy\"`\n\t\t\tXUserData      string `json:\"x:user_data\"`\n\t\t} `json:\"multi_parts\"`\n\t\tURL string `json:\"url\"`\n\t} `json:\"form\"`*/\n\n\t//UPLOAD_TYPE_RESUMABLE\n\tResumable struct {\n\t\tKind   string `json:\"kind\"`\n\t\tParams struct {\n\t\t\tAccessKeyID     string    `json:\"access_key_id\"`\n\t\t\tAccessKeySecret string    `json:\"access_key_secret\"`\n\t\t\tBucket          string    `json:\"bucket\"`\n\t\t\tEndpoint        string    `json:\"endpoint\"`\n\t\t\tExpiration      time.Time `json:\"expiration\"`\n\t\t\tKey             string    `json:\"key\"`\n\t\t\tSecurityToken   string    `json:\"security_token\"`\n\t\t} `json:\"params\"`\n\t\tProvider string `json:\"provider\"`\n\t} `json:\"resumable\"`\n\n\tFile Files `json:\"file\"`\n}\n\n// 添加离线下载响应\ntype OfflineDownloadResp struct {\n\tFile       *string     `json:\"file\"`\n\tTask       OfflineTask `json:\"task\"`\n\tUploadType string      `json:\"upload_type\"`\n\tURL        struct {\n\t\tKind string `json:\"kind\"`\n\t} `json:\"url\"`\n}\n\n// 离线下载列表\ntype OfflineListResp struct {\n\tExpiresIn     int64         `json:\"expires_in\"`\n\tNextPageToken string        `json:\"next_page_token\"`\n\tTasks         []OfflineTask `json:\"tasks\"`\n}\n\n// offlineTask\ntype OfflineTask struct {\n\tCallback    string   `json:\"callback\"`\n\tCreatedTime string   `json:\"created_time\"`\n\tFileID      string   `json:\"file_id\"`\n\tFileName    string   `json:\"file_name\"`\n\tFileSize    string   `json:\"file_size\"`\n\tIconLink    string   `json:\"icon_link\"`\n\tID          string   `json:\"id\"`\n\tKind        string   `json:\"kind\"`\n\tMessage     string   `json:\"message\"`\n\tName        string   `json:\"name\"`\n\tParams      Params   `json:\"params\"`\n\tPhase       string   `json:\"phase\"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING\n\tProgress    int64    `json:\"progress\"`\n\tSpace       string   `json:\"space\"`\n\tStatusSize  int64    `json:\"status_size\"`\n\tStatuses    []string `json:\"statuses\"`\n\tThirdTaskID string   `json:\"third_task_id\"`\n\tType        string   `json:\"type\"`\n\tUpdatedTime string   `json:\"updated_time\"`\n\tUserID      string   `json:\"user_id\"`\n}\n\ntype Params struct {\n\tFolderType   string `json:\"folder_type\"`\n\tPredictSpeed string `json:\"predict_speed\"`\n\tPredictType  string `json:\"predict_type\"`\n}\n\n// LoginReviewResp 登录验证响应\ntype LoginReviewResp struct {\n\tCreditkey        string `json:\"creditkey\"`\n\tError            string `json:\"error\"`\n\tErrorCode        string `json:\"errorCode\"`\n\tErrorDesc        string `json:\"errorDesc\"`\n\tErrorDescURL     string `json:\"errorDescUrl\"`\n\tErrorIsRetry     int    `json:\"errorIsRetry\"`\n\tErrorDescription string `json:\"error_description\"`\n\tIsCompressed     string `json:\"isCompressed\"`\n\tPlatformVersion  string `json:\"platformVersion\"`\n\tProtocolVersion  string `json:\"protocolVersion\"`\n\tReviewurl        string `json:\"reviewurl\"`\n\tSequenceNo       string `json:\"sequenceNo\"`\n\tUserID           string `json:\"userID\"`\n\tVerifyType       string `json:\"verifyType\"`\n}\n\n// ReviewData 验证数据\ntype ReviewData struct {\n\tCreditkey  string `json:\"creditkey\"`\n\tReviewurl  string `json:\"reviewurl\"`\n\tDeviceid   string `json:\"deviceid\"`\n\tDevicesign string `json:\"devicesign\"`\n}\n\ntype AboutResponse struct {\n\t// Kind  string `json:\"kind\"`\n\tQuota struct {\n\t\t// Kind           string `json:\"kind\"`\n\t\tLimit string `json:\"limit\"`\n\t\tUsage string `json:\"usage\"`\n\t\t// UsageInTrash   string `json:\"usage_in_trash\"`\n\t\t// PlayTimesLimit string `json:\"play_times_limit\"`\n\t\t// PlayTimesUsage string `json:\"play_times_usage\"`\n\t\t// IsUnlimited    bool   `json:\"is_unlimited\"`\n\t\t// UpgradeType    string `json:\"upgrade_type\"`\n\t} `json:\"quota\"`\n\t// ExpiresAt string `json:\"expires_at\"`\n\t// Quotas    struct {\n\t// } `json:\"quotas\"`\n\t// IsSearchFlushed bool `json:\"is_search_flushed\"`\n}\n"
  },
  {
    "path": "drivers/thunder/util.go",
    "content": "package thunder\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tAPI_URL             = \"https://api-pan.xunlei.com/drive/v1\"\n\tFILE_API_URL        = API_URL + \"/files\"\n\tTASK_API_URL        = API_URL + \"/tasks\"\n\tXLUSER_API_BASE_URL = \"https://xluser-ssl.xunlei.com\"\n\tXLUSER_API_URL      = XLUSER_API_BASE_URL + \"/v1\"\n)\n\nconst (\n\tFOLDER    = \"drive#folder\"\n\tFILE      = \"drive#file\"\n\tRESUMABLE = \"drive#resumable\"\n)\n\nconst (\n\tUPLOAD_TYPE_UNKNOWN = \"UPLOAD_TYPE_UNKNOWN\"\n\t//UPLOAD_TYPE_FORM      = \"UPLOAD_TYPE_FORM\"\n\tUPLOAD_TYPE_RESUMABLE = \"UPLOAD_TYPE_RESUMABLE\"\n\tUPLOAD_TYPE_URL       = \"UPLOAD_TYPE_URL\"\n)\n\nconst (\n\tSignProvider = \"access_end_point_token\"\n\tAPPID        = \"40\"\n\tAPPKey       = \"34a062aaa22f906fca4fefe9fb3a3021\"\n)\n\nfunc GetAction(method string, url string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(url)[1]\n\treturn method + \":\" + urlpath\n}\n\ntype Common struct {\n\tclient *resty.Client\n\n\tcaptchaToken string\n\n\tcreditKey string\n\n\t// 签名相关,二选一\n\tAlgorithms             []string\n\tTimestamp, CaptchaSign string\n\n\t// 必要值,签名相关\n\tDeviceID          string\n\tClientID          string\n\tClientSecret      string\n\tClientVersion     string\n\tPackageName       string\n\tUserAgent         string\n\tDownloadUserAgent string\n\tUseVideoUrl       bool\n\tSpace             string\n\n\t// 验证码token刷新成功回调\n\trefreshCTokenCk func(token string)\n}\n\nfunc (c *Common) SetCaptchaToken(captchaToken string) {\n\tc.captchaToken = captchaToken\n}\nfunc (c *Common) GetCaptchaToken() string {\n\treturn c.captchaToken\n}\n\nfunc (c *Common) SetCreditKey(creditKey string) {\n\tc.creditKey = creditKey\n}\nfunc (c *Common) GetCreditKey() string {\n\treturn c.creditKey\n}\n\n// 刷新验证码token(登录后)\nfunc (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {\n\tmetas := map[string]string{\n\t\t\"client_version\": c.ClientVersion,\n\t\t\"package_name\":   c.PackageName,\n\t\t\"user_id\":        userID,\n\t}\n\tmetas[\"timestamp\"], metas[\"captcha_sign\"] = c.GetCaptchaSign()\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// 刷新验证码token(登录时)\nfunc (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {\n\tmetas := make(map[string]string)\n\tif ok, _ := regexp.MatchString(`\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*`, username); ok {\n\t\tmetas[\"email\"] = username\n\t} else if len(username) >= 11 && len(username) <= 18 {\n\t\tmetas[\"phone_number\"] = username\n\t} else {\n\t\tmetas[\"username\"] = username\n\t}\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// 获取验证码签名\nfunc (c *Common) GetCaptchaSign() (timestamp, sign string) {\n\tif len(c.Algorithms) == 0 {\n\t\treturn c.Timestamp, c.CaptchaSign\n\t}\n\ttimestamp = fmt.Sprint(time.Now().UnixMilli())\n\tstr := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)\n\tfor _, algorithm := range c.Algorithms {\n\t\tstr = utils.GetMD5EncodeStr(str + algorithm)\n\t}\n\tsign = \"1.\" + str\n\treturn\n}\n\n// 刷新验证码token\nfunc (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {\n\tparam := CaptchaTokenRequest{\n\t\tAction:       action,\n\t\tCaptchaToken: c.captchaToken,\n\t\tClientID:     c.ClientID,\n\t\tDeviceID:     c.DeviceID,\n\t\tMeta:         metas,\n\t\tRedirectUri:  \"xlaccsdk01://xunlei.com/callback?state=harbor\",\n\t}\n\tvar e ErrResp\n\tvar resp CaptchaTokenResponse\n\t_, err := c.Request(XLUSER_API_URL+\"/shield/captcha/init\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetError(&e).SetBody(param)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif e.IsError() {\n\t\treturn &e\n\t}\n\n\tif resp.Url != \"\" {\n\t\treturn fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, resp.Url)\n\t}\n\n\tif resp.CaptchaToken == \"\" {\n\t\treturn fmt.Errorf(\"empty captchaToken\")\n\t}\n\n\tif c.refreshCTokenCk != nil {\n\t\tc.refreshCTokenCk(resp.CaptchaToken)\n\t}\n\tc.SetCaptchaToken(resp.CaptchaToken)\n\treturn nil\n}\n\n// 只有基础信息的请求\nfunc (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := c.client.R().SetHeaders(map[string]string{\n\t\t\"user-agent\":       c.UserAgent,\n\t\t\"accept\":           \"application/json;charset=UTF-8\",\n\t\t\"x-device-id\":      c.DeviceID,\n\t\t\"x-client-id\":      c.ClientID,\n\t\t\"x-client-version\": c.ClientVersion,\n\t})\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar erron ErrResp\n\tutils.Json.Unmarshal(res.Body(), &erron)\n\tif erron.IsError() {\n\t\t// review_panel 表示需要短信验证码进行验证\n\t\tif erron.ErrorMsg == \"review_panel\" {\n\t\t\treturn nil, c.getReviewData(res)\n\t\t}\n\n\t\treturn nil, &erron\n\t}\n\n\treturn res.Body(), nil\n}\n\n// 获取验证所需内容\nfunc (c *Common) getReviewData(res *resty.Response) error {\n\tvar reviewResp LoginReviewResp\n\tvar reviewData ReviewData\n\n\tif err := utils.Json.Unmarshal(res.Body(), &reviewResp); err != nil {\n\t\treturn err\n\t}\n\n\tdeviceSign := generateDeviceSign(c.DeviceID, c.PackageName)\n\n\treviewData = ReviewData{\n\t\tCreditkey:  reviewResp.Creditkey,\n\t\tReviewurl:  reviewResp.Reviewurl + \"&deviceid=\" + deviceSign,\n\t\tDeviceid:   deviceSign,\n\t\tDevicesign: deviceSign,\n\t}\n\n\t// 将reviewData转为JSON字符串\n\treviewDataJSON, _ := json.MarshalIndent(reviewData, \"\", \"  \")\n\t//reviewDataJSON, _ := json.Marshal(reviewData)\n\n\treturn fmt.Errorf(`\n<div style=\"font-family: Arial, sans-serif; padding: 15px; border-radius: 5px; border: 1px solid #e0e0e0;>\n    <h3 style=\"color: #d9534f; margin-top: 0;\">\n        <span style=\"font-size: 16px;\">🔒 本次登录需要验证</span><br>\n        <span style=\"font-size: 14px; font-weight: normal; color: #666;\">This login requires verification</span>\n    </h3>\n    <p style=\"font-size: 14px; margin-bottom: 15px;\">下面是验证所需要的数据，具体使用方法请参照对应的驱动文档<br>\n    <span style=\"color: #666; font-size: 13px;\">Below are the relevant verification data. For specific usage methods, please refer to the corresponding driver documentation.</span></p>\n    <div style=\"border: 1px solid #ddd; border-radius: 4px; padding: 10px; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 13px;\">\n        <pre style=\"margin: 0; white-space: pre-wrap;\"><code>%s</code></pre>\n    </div>\n</div>`, string(reviewDataJSON))\n}\n\n// 计算文件Gcid\nfunc getGcid(r io.Reader, size int64) (string, error) {\n\tcalcBlockSize := func(j int64) int64 {\n\t\tvar psize int64 = 0x40000\n\t\tfor float64(j)/float64(psize) > 0x200 && psize < 0x200000 {\n\t\t\tpsize = psize << 1\n\t\t}\n\t\treturn psize\n\t}\n\n\thash1 := sha1.New()\n\thash2 := sha1.New()\n\treadSize := calcBlockSize(size)\n\tfor {\n\t\thash2.Reset()\n\t\tif n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {\n\t\t\tif err != io.EOF {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\thash1.Write(hash2.Sum(nil))\n\t}\n\treturn hex.EncodeToString(hash1.Sum(nil)), nil\n}\n\nfunc generateDeviceSign(deviceID, packageName string) string {\n\n\tsignatureBase := fmt.Sprintf(\"%s%s%s%s\", deviceID, packageName, APPID, APPKey)\n\n\tsha1Hash := sha1.New()\n\tsha1Hash.Write([]byte(signatureBase))\n\tsha1Result := sha1Hash.Sum(nil)\n\n\tsha1String := hex.EncodeToString(sha1Result)\n\n\tmd5Hash := md5.New()\n\tmd5Hash.Write([]byte(sha1String))\n\tmd5Result := md5Hash.Sum(nil)\n\n\tmd5String := hex.EncodeToString(md5Result)\n\n\tdeviceSign := fmt.Sprintf(\"div101.%s%s\", deviceID, md5String)\n\n\treturn deviceSign\n}\n"
  },
  {
    "path": "drivers/thunder_browser/driver.go",
    "content": "package thunder_browser\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tstreamPkg \"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype ThunderBrowser struct {\n\t*XunLeiBrowserCommon\n\tmodel.Storage\n\tAddition\n\n\tidentity string\n}\n\nfunc (x *ThunderBrowser) Config() driver.Config {\n\treturn config\n}\n\nfunc (x *ThunderBrowser) GetAddition() driver.Additional {\n\treturn &x.Addition\n}\n\nfunc (x *ThunderBrowser) Init(ctx context.Context) (err error) {\n\n\tspaceTokenFunc := func() error {\n\t\t// 如果用户未设置 \"超级保险柜\" 密码 则直接返回\n\t\tif x.SafePassword == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\t// 通过 GetSafeAccessToken 获取\n\t\ttoken, err := x.GetSafeAccessToken(x.SafePassword)\n\t\tx.SetSpaceTokenResp(token)\n\t\treturn err\n\t}\n\n\t// 初始化所需参数\n\tif x.XunLeiBrowserCommon == nil {\n\t\tx.XunLeiBrowserCommon = &XunLeiBrowserCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient:            base.NewRestyClient(),\n\t\t\t\tAlgorithms:        Algorithms,\n\t\t\t\tDeviceID:          utils.GetMD5EncodeStr(x.Username + x.Password),\n\t\t\t\tClientID:          ClientID,\n\t\t\t\tClientSecret:      ClientSecret,\n\t\t\t\tClientVersion:     ClientVersion,\n\t\t\t\tPackageName:       PackageName,\n\t\t\t\tUserAgent:         BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), PackageName, SdkVersion, ClientVersion, PackageName),\n\t\t\t\tDownloadUserAgent: DownloadUserAgent,\n\t\t\t\tUseVideoUrl:       x.UseVideoUrl,\n\t\t\t\tUseFluentPlay:     x.UseFluentPlay,\n\t\t\t\tRemoveWay:         x.Addition.RemoveWay,\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t\trefreshTokenFunc: func() error {\n\t\t\t\t// 通过RefreshToken刷新\n\t\t\t\ttoken, err := x.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// 重新登录\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t\t}\n\t\t\t\t\t// 清空 信任密钥\n\t\t\t\t\tx.Addition.CreditKey = \"\"\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\treturn err\n\t\t\t},\n\t\t}\n\t}\n\n\t// 自定义验证码token\n\tctoekn := strings.TrimSpace(x.CaptchaToken)\n\tif ctoekn != \"\" {\n\t\tx.SetCaptchaToken(ctoekn)\n\t}\n\n\tif x.Addition.CreditKey != \"\" {\n\t\tx.SetCreditKey(x.Addition.CreditKey)\n\t}\n\n\tif x.Addition.DeviceID != \"\" {\n\t\tx.Common.DeviceID = x.Addition.DeviceID\n\t} else {\n\t\tx.Addition.DeviceID = x.Common.DeviceID\n\t\top.MustSaveDriverStorage(x)\n\t}\n\n\tx.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl\n\tx.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay\n\tx.Addition.RootFolderID = x.RootFolderID\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif x.identity != identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\t// 登录\n\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// 清空 信任密钥\n\t\tx.Addition.CreditKey = \"\"\n\t\tx.SetTokenResp(token)\n\t}\n\n\t// 获取 spaceToken\n\terr = spaceTokenFunc()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (x *ThunderBrowser) Drop(ctx context.Context) error {\n\treturn nil\n}\n\ntype ThunderBrowserExpert struct {\n\t*XunLeiBrowserCommon\n\tmodel.Storage\n\tExpertAddition\n\n\tidentity string\n}\n\nfunc (x *ThunderBrowserExpert) Config() driver.Config {\n\treturn configExpert\n}\n\nfunc (x *ThunderBrowserExpert) GetAddition() driver.Additional {\n\treturn &x.ExpertAddition\n}\n\nfunc (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {\n\n\tspaceTokenFunc := func() error {\n\t\t// 如果用户未设置 \"超级保险柜\" 密码 则直接返回\n\t\tif x.SafePassword == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\t// 通过 GetSafeAccessToken 获取\n\t\ttoken, err := x.GetSafeAccessToken(x.SafePassword)\n\t\tx.SetSpaceTokenResp(token)\n\t\treturn err\n\t}\n\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif identity != x.identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\tx.XunLeiBrowserCommon = &XunLeiBrowserCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient: base.NewRestyClient(),\n\t\t\t\tDeviceID: func() string {\n\t\t\t\t\tif len(x.DeviceID) != 32 {\n\t\t\t\t\t\tif x.LoginType == \"user\" {\n\t\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.Username + x.Password)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)\n\t\t\t\t\t}\n\t\t\t\t\treturn x.DeviceID\n\t\t\t\t}(),\n\t\t\t\tClientID:      x.ClientID,\n\t\t\t\tClientSecret:  x.ClientSecret,\n\t\t\t\tClientVersion: x.ClientVersion,\n\t\t\t\tPackageName:   x.PackageName,\n\t\t\t\tUserAgent: func() string {\n\t\t\t\t\tif x.ExpertAddition.UserAgent != \"\" {\n\t\t\t\t\t\treturn x.ExpertAddition.UserAgent\n\t\t\t\t\t}\n\t\t\t\t\tif x.LoginType == \"user\" {\n\t\t\t\t\t\treturn BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)\n\t\t\t\t\t}\n\t\t\t\t\treturn BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)\n\t\t\t\t}(),\n\t\t\t\tDownloadUserAgent: func() string {\n\t\t\t\t\tif x.ExpertAddition.DownloadUserAgent != \"\" {\n\t\t\t\t\t\treturn x.ExpertAddition.DownloadUserAgent\n\t\t\t\t\t}\n\t\t\t\t\treturn DownloadUserAgent\n\t\t\t\t}(),\n\t\t\t\tUseVideoUrl:   x.UseVideoUrl,\n\t\t\t\tUseFluentPlay: x.UseFluentPlay,\n\t\t\t\tRemoveWay:     x.ExpertAddition.RemoveWay,\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tif x.ExpertAddition.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.ExpertAddition.CaptchaToken)\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tif x.ExpertAddition.CreditKey != \"\" {\n\t\t\tx.SetCreditKey(x.ExpertAddition.CreditKey)\n\t\t}\n\n\t\tif x.ExpertAddition.DeviceID != \"\" {\n\t\t\tx.Common.DeviceID = x.ExpertAddition.DeviceID\n\t\t} else {\n\t\t\tx.ExpertAddition.DeviceID = x.Common.DeviceID\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tif x.Common.UserAgent != \"\" {\n\t\t\tx.ExpertAddition.UserAgent = x.Common.UserAgent\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tif x.Common.DownloadUserAgent != \"\" {\n\t\t\tx.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tx.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl\n\t\tx.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay\n\t\tx.ExpertAddition.RootFolderID = x.RootFolderID\n\t\t// 签名方法\n\t\tif x.SignType == \"captcha_sign\" {\n\t\t\tx.Common.Timestamp = x.Timestamp\n\t\t\tx.Common.CaptchaSign = x.CaptchaSign\n\t\t} else {\n\t\t\tx.Common.Algorithms = strings.Split(x.Algorithms, \",\")\n\t\t}\n\n\t\t// 登录方式\n\t\tif x.LoginType == \"refresh_token\" {\n\t\t\t// 通过RefreshToken登录\n\t\t\ttoken, err := x.XunLeiBrowserCommon.RefreshToken(x.ExpertAddition.RefreshToken)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tx.SetTokenResp(token)\n\n\t\t\t// 刷新token方法\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\n\t\t\terr = spaceTokenFunc()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t} else {\n\t\t\t// 通过用户密码登录\n\t\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// 清空 信任密钥\n\t\t\tx.ExpertAddition.CreditKey = \"\"\n\t\t\tx.SetTokenResp(token)\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t}\n\t\t\t\t\t// 清空 信任密钥\n\t\t\t\t\tx.ExpertAddition.CreditKey = \"\"\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\n\t\t\terr = spaceTokenFunc()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// 仅修改验证码token\n\t\tif x.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.CaptchaToken)\n\t\t}\n\n\t\terr = spaceTokenFunc()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tx.XunLeiBrowserCommon.UserAgent = x.UserAgent\n\t\tx.XunLeiBrowserCommon.DownloadUserAgent = x.DownloadUserAgent\n\t\tx.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl\n\t\tx.XunLeiBrowserCommon.UseFluentPlay = x.UseFluentPlay\n\t\tx.ExpertAddition.RootFolderID = x.RootFolderID\n\t}\n\n\treturn nil\n}\n\nfunc (x *ThunderBrowserExpert) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {\n\tx.XunLeiBrowserCommon.SetTokenResp(token)\n\tif token != nil {\n\t\tx.ExpertAddition.RefreshToken = token.RefreshToken\n\t}\n}\n\ntype XunLeiBrowserCommon struct {\n\t*Common\n\t*TokenResp     // 登录信息\n\t*CoreLoginResp // core登录信息\n\n\trefreshTokenFunc func() error\n}\n\nfunc (xc *XunLeiBrowserCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn xc.getFiles(ctx, dir, args.ReqPath)\n}\n\nfunc (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar lFile Files\n\n\tparams := map[string]string{\n\t\t\"_magic\":         \"2021\",\n\t\t\"space\":          file.(*Files).GetSpace(),\n\t\t\"thumbnail_size\": \"SIZE_LARGE\",\n\t\t\"with\":           \"url\",\n\t}\n\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", file.GetID())\n\t\tr.SetQueryParams(params)\n\t\t//r.SetQueryParam(\"space\", \"\")\n\t}, &lFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := &model.Link{\n\t\tURL: lFile.WebContentLink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": {xc.DownloadUserAgent},\n\t\t},\n\t}\n\n\tif xc.UseVideoUrl {\n\t\tfor _, media := range lFile.Medias {\n\t\t\tif media.Link.URL != \"\" {\n\t\t\t\tlink.URL = media.Link.URL\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn link, nil\n}\n\nfunc (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tjs := base.Json{\n\t\t\"kind\":      FOLDER,\n\t\t\"name\":      dirName,\n\t\t\"parent_id\": parentDir.GetID(),\n\t\t\"space\":     parentDir.(*Files).GetSpace(),\n\t}\n\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&js)\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\n\tparams := map[string]string{\n\t\t\"_from\": srcObj.(*Files).GetSpace(),\n\t}\n\tjs := base.Json{\n\t\t\"to\":    base.Json{\"parent_id\": dstDir.GetID(), \"space\": dstDir.(*Files).GetSpace()},\n\t\t\"space\": srcObj.(*Files).GetSpace(),\n\t\t\"ids\":   []string{srcObj.GetID()},\n\t}\n\n\t_, err := xc.Request(FILE_API_URL+\":batchMove\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&js)\n\t\tr.SetQueryParams(params)\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\n\tparams := map[string]string{\n\t\t\"space\": srcObj.(*Files).GetSpace(),\n\t}\n\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodPatch, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", srcObj.GetID())\n\t\tr.SetBody(&base.Json{\"name\": newName})\n\t\tr.SetQueryParams(params)\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiBrowserCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\n\tparams := map[string]string{\n\t\t\"_from\": srcObj.(*Files).GetSpace(),\n\t}\n\tjs := base.Json{\n\t\t\"to\":    base.Json{\"parent_id\": dstDir.GetID(), \"space\": dstDir.(*Files).GetSpace()},\n\t\t\"space\": srcObj.(*Files).GetSpace(),\n\t\t\"ids\":   []string{srcObj.GetID()},\n\t}\n\n\t_, err := xc.Request(FILE_API_URL+\":batchCopy\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&js)\n\t\tr.SetQueryParams(params)\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiBrowserCommon) Remove(ctx context.Context, obj model.Obj) error {\n\n\tjs := base.Json{\n\t\t\"ids\":   []string{obj.GetID()},\n\t\t\"space\": obj.(*Files).GetSpace(),\n\t}\n\t// 先判断是否是特殊情况\n\tif obj.(*Files).GetSpace() == ThunderDriveSpace {\n\t\t_, err := xc.Request(FILE_API_URL+\"/{fileID}/trash\", http.MethodPatch, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetPathParam(\"fileID\", obj.GetID())\n\t\t\tr.SetBody(\"{}\")\n\t\t}, nil)\n\t\treturn err\n\t} else if obj.(*Files).GetSpace() == ThunderBrowserDriveSafeSpace || obj.(*Files).GetSpace() == ThunderDriveSafeSpace {\n\t\t_, err := xc.Request(FILE_API_URL+\":batchDelete\", http.MethodPost, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetBody(&js)\n\t\t}, nil)\n\t\treturn err\n\t}\n\n\t// 根据用户选择的删除方式进行删除\n\tif xc.RemoveWay == \"delete\" {\n\t\t_, err := xc.Request(FILE_API_URL+\":batchDelete\", http.MethodPost, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetBody(&js)\n\t\t}, nil)\n\t\treturn err\n\t} else {\n\t\t_, err := xc.Request(FILE_API_URL+\":batchTrash\", http.MethodPost, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetBody(&js)\n\t\t}, nil)\n\t\treturn err\n\t}\n}\n\nfunc (xc *XunLeiBrowserCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tgcid := stream.GetHash().GetHash(hash_extend.GCID)\n\tvar err error\n\tif len(gcid) < hash_extend.GCID.Width {\n\t\t_, gcid, err = streamPkg.CacheFullAndHash(stream, &up, hash_extend.GCID, stream.GetSize())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tjs := base.Json{\n\t\t\"kind\":        FILE,\n\t\t\"parent_id\":   dstDir.GetID(),\n\t\t\"name\":        stream.GetName(),\n\t\t\"size\":        stream.GetSize(),\n\t\t\"hash\":        gcid,\n\t\t\"upload_type\": UPLOAD_TYPE_RESUMABLE,\n\t\t\"space\":       dstDir.(*Files).GetSpace(),\n\t}\n\n\tvar resp UploadTaskResponse\n\t_, err = xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&js)\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparam := resp.Resumable.Params\n\tif resp.UploadType == UPLOAD_TYPE_RESUMABLE {\n\t\tparam.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+\".\")\n\t\ts, err := session.NewSession(&aws.Config{\n\t\t\tCredentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),\n\t\t\tRegion:      aws.String(\"xunlei\"),\n\t\t\tEndpoint:    aws.String(param.Endpoint),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuploader := s3manager.NewUploader(s)\n\t\tif stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\t\tuploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)\n\t\t}\n\t\t_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{\n\t\t\tBucket:  aws.String(param.Bucket),\n\t\t\tKey:     aws.String(param.Key),\n\t\t\tExpires: aws.Time(param.Expiration),\n\t\t\tBody:    driver.NewLimitedUploadStream(ctx, io.TeeReader(stream, driver.NewProgress(stream.GetSize(), up))),\n\t\t})\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (xc *XunLeiBrowserCommon) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tvar about AboutResponse\n\t_, err := xc.Request(API_URL+\"/about\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t}, &about)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttotal, err := strconv.ParseInt(about.Quota.Limit, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(about.Quota.Usage, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nfunc (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, dir model.Obj, path string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tvar pageToken string\n\tfor {\n\t\tvar fileList FileList\n\t\tfolderSpace := \"\"\n\t\tswitch dirF := dir.(type) {\n\t\tcase *Files:\n\t\t\tfolderSpace = dirF.GetSpace()\n\t\tdefault:\n\t\t\t// 处理 根目录的情况\n\t\t\t//folderSpace = ThunderBrowserDriveSpace\n\t\t\tfolderSpace = ThunderDriveSpace // 迅雷浏览器已经合并到迅雷云盘，因此变更根目录\n\t\t}\n\t\tparams := map[string]string{\n\t\t\t\"parent_id\":      dir.GetID(),\n\t\t\t\"page_token\":     pageToken,\n\t\t\t\"space\":          folderSpace,\n\t\t\t\"filters\":        `{\"trashed\":{\"eq\":false}}`,\n\t\t\t\"with\":           \"url\",\n\t\t\t\"with_audit\":     \"true\",\n\t\t\t\"thumbnail_size\": \"SIZE_LARGE\",\n\t\t}\n\n\t\t_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(params)\n\t\t}, &fileList)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := range fileList.Files {\n\t\t\t// 解决 \"迅雷云盘\" 重复出现问题————迅雷后端发送错误\n\t\t\tif fileList.Files[i].FolderType == ThunderDriveFolderType && fileList.Files[i].ID == \"\" && fileList.Files[i].Space == \"\" && dir.GetID() != \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfiles = append(files, &fileList.Files[i])\n\t\t}\n\n\t\tif fileList.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tpageToken = fileList.NextPageToken\n\t}\n\treturn files, nil\n}\n\n// SetRefreshTokenFunc 设置刷新Token的方法\nfunc (xc *XunLeiBrowserCommon) SetRefreshTokenFunc(fn func() error) {\n\txc.refreshTokenFunc = fn\n}\n\n// SetTokenResp 设置Token\nfunc (xc *XunLeiBrowserCommon) SetTokenResp(tr *TokenResp) {\n\txc.TokenResp = tr\n}\n\n// SetCoreTokenResp 设置CoreToken\nfunc (xc *XunLeiBrowserCommon) SetCoreTokenResp(tr *CoreLoginResp) {\n\txc.CoreLoginResp = tr\n}\n\n// SetSpaceTokenResp 设置Token\nfunc (xc *XunLeiBrowserCommon) SetSpaceTokenResp(spaceToken string) {\n\txc.TokenResp.Token = spaceToken\n}\n\n// Request 携带Authorization和CaptchaToken的请求\nfunc (xc *XunLeiBrowserCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tdata, err := xc.Common.Request(url, method, func(req *resty.Request) {\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Authorization\":         xc.GetToken(),\n\t\t\t\"X-Captcha-Token\":       xc.GetCaptchaToken(),\n\t\t\t\"X-Space-Authorization\": xc.GetSpaceToken(),\n\t\t})\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t}, resp)\n\n\terrResp, ok := err.(*ErrResp)\n\tif !ok {\n\t\treturn nil, err\n\t}\n\n\tswitch errResp.ErrorCode {\n\tcase 0:\n\t\treturn data, nil\n\tcase 4122, 4121, 10, 16:\n\t\tif xc.refreshTokenFunc != nil {\n\t\t\tif err = xc.refreshTokenFunc(); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\tcase 9:\n\t\t// space_token 获取失败\n\t\tif errResp.ErrorMsg == \"space_token_invalid\" {\n\t\t\tif token, err := xc.GetSafeAccessToken(xc.Token); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\txc.SetSpaceTokenResp(token)\n\t\t\t}\n\n\t\t}\n\t\tif errResp.ErrorMsg == \"captcha_invalid\" {\n\t\t\t// 验证码token过期\n\t\t\tif err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.TokenResp.UserID); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\treturn nil, errors.New(errResp.ErrorMsg)\n\tdefault:\n\t\t// 处理未捕获到的验证码错误\n\t\tif errResp.ErrorMsg == \"captcha_invalid\" {\n\t\t\t// 验证码token过期\n\t\t\tif err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.TokenResp.UserID); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn xc.Request(url, method, callback, resp)\n}\n\n// RefreshToken 刷新Token\nfunc (xc *XunLeiBrowserCommon) RefreshToken(refreshToken string) (*TokenResp, error) {\n\tvar resp TokenResp\n\t_, err := xc.Common.Request(XLUSER_API_URL+\"/auth/token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(&base.Json{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": refreshToken,\n\t\t\t\"client_id\":     xc.ClientID,\n\t\t\t\"client_secret\": xc.ClientSecret,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.RefreshToken == \"\" {\n\t\treturn nil, errors.New(\"refresh token is empty\")\n\t}\n\treturn &resp, nil\n}\n\n// GetSafeAccessToken 获取 超级保险柜 AccessToken\nfunc (xc *XunLeiBrowserCommon) GetSafeAccessToken(safePassword string) (string, error) {\n\tvar resp TokenResp\n\t_, err := xc.Request(XLUSER_API_URL+\"/password/check\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(&base.Json{\n\t\t\t\"scene\":    \"box\",\n\t\t\t\"password\": EncryptPassword(safePassword),\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif resp.Token == \"\" {\n\t\treturn \"\", errors.New(\"SafePassword is incorrect \")\n\t}\n\treturn resp.Token, nil\n}\n\n// Login 登录\nfunc (xc *XunLeiBrowserCommon) Login(username, password string) (*TokenResp, error) {\n\t//v3 login拿到 sessionID\n\tsessionID, err := xc.CoreLogin(username, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//v1 login拿到令牌\n\turl := XLUSER_API_URL + \"/auth/signin/token\"\n\tif err = xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp TokenResp\n\t_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetPathParam(\"client_id\", xc.ClientID)\n\t\treq.SetBody(&SignInRequest{\n\t\t\tClientID:     xc.ClientID,\n\t\t\tClientSecret: xc.ClientSecret,\n\t\t\tProvider:     SignProvider,\n\t\t\tSigninToken:  sessionID,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (xc *XunLeiBrowserCommon) IsLogin() bool {\n\tif xc.TokenResp == nil {\n\t\treturn false\n\t}\n\t_, err := xc.Request(XLUSER_API_URL+\"/user/me\", http.MethodGet, nil, nil)\n\treturn err == nil\n}\n\n// OfflineDownload 离线下载文件\nfunc (xc *XunLeiBrowserCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {\n\tvar resp OfflineDownloadResp\n\n\tbody := base.Json{}\n\n\tfrom := \"cloudadd/\"\n\n\tif xc.UseFluentPlay {\n\t\tbody = base.Json{\n\t\t\t\"kind\": FILE,\n\t\t\t\"name\": fileName,\n\t\t\t// 流畅播接口 强制将文件放在 \"SPACE_FAVORITE\" 文件夹\n\t\t\t//\"parent_id\":   parentDir.GetID(),\n\t\t\t\"upload_type\": UPLOAD_TYPE_URL,\n\t\t\t\"url\": base.Json{\n\t\t\t\t\"url\": fileUrl,\n\t\t\t\t//\"files\": []string{\"0\"}, // 0 表示只下载第一个文件\n\t\t\t},\n\t\t\t\"params\": base.Json{\n\t\t\t\t\"cookie\":      \"null\",\n\t\t\t\t\"web_title\":   \"\",\n\t\t\t\t\"lastSession\": \"\",\n\t\t\t\t\"flags\":       \"9\",\n\t\t\t\t\"scene\":       \"smart_spot_panel\",\n\t\t\t\t\"referer\":     \"https://x.xunlei.com\",\n\t\t\t\t\"dedup_index\": \"0\",\n\t\t\t},\n\t\t\t\"need_dedup\":  true,\n\t\t\t\"folder_type\": \"FAVORITE\",\n\t\t\t\"space\":       ThunderBrowserDriveFluentPlayFolderType,\n\t\t}\n\n\t\tfrom = \"FLUENT_PLAY/sniff_ball/fluent_play/SPACE_FAVORITE\"\n\t} else {\n\t\tbody = base.Json{\n\t\t\t\"kind\":        FILE,\n\t\t\t\"name\":        fileName,\n\t\t\t\"parent_id\":   parentDir.GetID(),\n\t\t\t\"upload_type\": UPLOAD_TYPE_URL,\n\t\t\t\"url\": base.Json{\n\t\t\t\t\"url\": fileUrl,\n\t\t\t},\n\t\t}\n\n\t\tif files, ok := parentDir.(*Files); ok {\n\t\t\tbody[\"space\"] = files.GetSpace()\n\t\t} else {\n\t\t\t// 如果不是 Files 类型，则默认使用 ThunderDriveSpace\n\t\t\tbody[\"space\"] = ThunderDriveSpace\n\t\t}\n\t}\n\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetQueryParam(\"_from\", from)\n\t\tr.SetBody(&body)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Task, err\n}\n\n// OfflineList 获取离线下载任务列表\nfunc (xc *XunLeiBrowserCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {\n\tres := make([]OfflineTask, 0)\n\n\tvar resp OfflineListResp\n\t_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"type\":       \"offline\",\n\t\t\t\t\"limit\":      \"10000\",\n\t\t\t\t\"page_token\": nextPageToken,\n\t\t\t\t\"space\":      \"default/*\",\n\t\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get offline list: %w\", err)\n\t}\n\tres = append(res, resp.Tasks...)\n\n\treturn res, nil\n}\n\nfunc (xc *XunLeiBrowserCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string) error {\n\tqueryParams := map[string]string{\n\t\t\"task_ids\": strings.Join(taskIDs, \",\"),\n\t\t\"_t\":       strconv.FormatInt(time.Now().UnixMilli(), 10),\n\t}\n\tif xc.UseFluentPlay {\n\t\tqueryParams[\"space\"] = ThunderBrowserDriveFluentPlayFolderType\n\t}\n\n\t_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(queryParams)\n\t}, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete tasks %v: %w\", taskIDs, err)\n\t}\n\n\treturn nil\n}\n\nfunc (xc *XunLeiBrowserCommon) CoreLogin(username string, password string) (sessionID string, err error) {\n\turl := XLUSER_API_BASE_URL + \"/xluser.core.login/v3/login\"\n\tvar resp CoreLoginResp\n\tres, err := xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"User-Agent\", \"android-ok-http-client/xl-acc-sdk/version-5.0.9.509300\")\n\t\treq.SetBody(&CoreLoginRequest{\n\t\t\tProtocolVersion: \"301\",\n\t\t\tSequenceNo:      \"1000010\",\n\t\t\tPlatformVersion: \"10\",\n\t\t\tIsCompressed:    \"0\",\n\t\t\tAppid:           APPID,\n\t\t\tClientVersion:   xc.Common.ClientVersion,\n\t\t\tPeerID:          \"00000000000000000000000000000000\",\n\t\t\tAppName:         \"ANDROID-com.xunlei.browser\",\n\t\t\tSdkVersion:      \"509300\",\n\t\t\tDevicesign:      generateDeviceSign(xc.DeviceID, xc.PackageName),\n\t\t\tNetWorkType:     \"WIFI\",\n\t\t\tProviderName:    \"NONE\",\n\t\t\tDeviceModel:     \"M2004J7AC\",\n\t\t\tDeviceName:      \"Xiaomi_M2004j7ac\",\n\t\t\tOSVersion:       \"12\",\n\t\t\tCreditkey:       xc.GetCreditKey(),\n\t\t\tHl:              \"zh-CN\",\n\t\t\tUserName:        username,\n\t\t\tPassWord:        password,\n\t\t\tVerifyKey:       \"\",\n\t\t\tVerifyCode:      \"\",\n\t\t\tIsMd5Pwd:        \"0\",\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = utils.Json.Unmarshal(res, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\txc.SetCoreTokenResp(&resp)\n\n\tsessionID = resp.SessionID\n\n\treturn sessionID, nil\n}\n"
  },
  {
    "path": "drivers/thunder_browser/meta.go",
    "content": "package thunder_browser\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// ExpertAddition 高级设置\ntype ExpertAddition struct {\n\tdriver.RootID\n\n\tLoginType string `json:\"login_type\" type:\"select\" options:\"user,refresh_token\" default:\"user\"`\n\tSignType  string `json:\"sign_type\" type:\"select\" options:\"algorithms,captcha_sign\" default:\"algorithms\"`\n\n\t// 登录方式1\n\tUsername string `json:\"username\" required:\"true\" help:\"login type is user,this is required\"`\n\tPassword string `json:\"password\" required:\"true\" help:\"login type is user,this is required\"`\n\t// 登录方式2\n\tRefreshToken string `json:\"refresh_token\" required:\"true\" help:\"login type is refresh_token,this is required\"`\n\n\tSafePassword string `json:\"safe_password\" required:\"true\" help:\"super safe password\"` // 超级保险箱密码\n\n\t// 签名方法1\n\tAlgorithms string `json:\"algorithms\" required:\"true\" help:\"sign type is algorithms,this is required\" default:\"Cw4kArmKJ/aOiFTxnQ0ES+D4mbbrIUsFn,HIGg0Qfbpm5ThZ/RJfjoao4YwgT9/M,u/PUD,OlAm8tPkOF1qO5bXxRN2iFttuDldrg,FFIiM6sFhWhU7tIMVUKOF7CUv/KzgwwV8FE,yN,4m5mglrIHksI6wYdq,LXEfS7,T+p+C+F2yjgsUtiXWU/cMNYEtJI4pq7GofW,14BrGIEMXkbvFvZ49nDUfVCRcHYFOJ1BP1Y,kWIH3Row,RAmRTKNCjucPWC\"`\n\t// 签名方法2\n\tCaptchaSign string `json:\"captcha_sign\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\tTimestamp   string `json:\"timestamp\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\n\t// 验证码\n\tCaptchaToken string `json:\"captcha_token\"`\n\t// 信任密钥\n\tCreditKey string `json:\"credit_key\" help:\"credit key,used for login\"`\n\n\t// 必要且影响登录,由签名决定\n\tDeviceID      string `json:\"device_id\"  required:\"false\" default:\"\"`\n\tClientID      string `json:\"client_id\"  required:\"true\" default:\"ZUBzD9J_XPXfn7f7\"`\n\tClientSecret  string `json:\"client_secret\"  required:\"true\" default:\"yESVmHecEe6F0aou69vl-g\"`\n\tClientVersion string `json:\"client_version\"  required:\"true\" default:\"1.40.0.7208\"`\n\tPackageName   string `json:\"package_name\"  required:\"true\" default:\"com.xunlei.browser\"`\n\n\t// 不影响登录,影响下载速度\n\tUserAgent         string `json:\"user_agent\"  required:\"false\" default:\"\"`\n\tDownloadUserAgent string `json:\"download_user_agent\"  required:\"false\" default:\"\"`\n\n\t// 优先使用视频链接代替下载链接\n\tUseVideoUrl bool `json:\"use_video_url\"`\n\t// 离线下载是否使用 流畅播(Fluent Play)接口\n\tUseFluentPlay bool `json:\"use_fluent_play\" default:\"false\" help:\"use fluent play for offline download,only magnet links supported\"`\n\t// 移除方式\n\tRemoveWay string `json:\"remove_way\" required:\"true\" type:\"select\" options:\"trash,delete\"`\n}\n\n// GetIdentity 登录特征,用于判断是否重新登录\nfunc (i *ExpertAddition) GetIdentity() string {\n\thash := md5.New()\n\tif i.LoginType == \"refresh_token\" {\n\t\thash.Write([]byte(i.RefreshToken))\n\t} else {\n\t\thash.Write([]byte(i.Username + i.Password))\n\t}\n\n\tif i.SignType == \"captcha_sign\" {\n\t\thash.Write([]byte(i.CaptchaSign + i.Timestamp))\n\t} else {\n\t\thash.Write([]byte(i.Algorithms))\n\t}\n\n\thash.Write([]byte(i.DeviceID))\n\thash.Write([]byte(i.ClientID))\n\thash.Write([]byte(i.ClientSecret))\n\thash.Write([]byte(i.ClientVersion))\n\thash.Write([]byte(i.PackageName))\n\treturn hex.EncodeToString(hash.Sum(nil))\n}\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername     string `json:\"username\" required:\"true\"`\n\tPassword     string `json:\"password\" required:\"true\"`\n\tSafePassword string `json:\"safe_password\" required:\"true\"` // 超级保险箱密码\n\tCaptchaToken string `json:\"captcha_token\"`\n\tCreditKey    string `json:\"credit_key\" help:\"credit key,used for login\"` // 信任密钥\n\tDeviceID     string `json:\"device_id\" default:\"\"`                        // 登录设备ID\n\tUseVideoUrl  bool   `json:\"use_video_url\" default:\"false\"`\n\t// 离线下载是否使用 流畅播(Fluent Play)接口\n\tUseFluentPlay bool   `json:\"use_fluent_play\" default:\"false\" help:\"use fluent play for offline download,only magnet links supported\"`\n\tRemoveWay     string `json:\"remove_way\" required:\"true\" type:\"select\" options:\"trash,delete\"`\n}\n\n// GetIdentity 登录特征,用于判断是否重新登录\nfunc (i *Addition) GetIdentity() string {\n\treturn utils.GetMD5EncodeStr(i.Username + i.Password)\n}\n\nvar config = driver.Config{\n\tName:      \"ThunderBrowser\",\n\tLocalSort: true,\n}\n\nvar configExpert = driver.Config{\n\tName:      \"ThunderBrowserExpert\",\n\tLocalSort: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ThunderBrowser{}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ThunderBrowserExpert{}\n\t})\n}\n"
  },
  {
    "path": "drivers/thunder_browser/types.go",
    "content": "package thunder_browser\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n)\n\ntype ErrResp struct {\n\tErrorCode        int64  `json:\"error_code\"`\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n\t//\tErrorDetails   interface{} `json:\"error_details\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\tif e.ErrorMsg == \"success\" {\n\t\treturn false\n\t}\n\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ErrorDescription != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ErrorDescription: %s \", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)\n}\n\n/*\n* 验证码Token\n**/\ntype CaptchaTokenRequest struct {\n\tAction       string            `json:\"action\"`\n\tCaptchaToken string            `json:\"captcha_token\"`\n\tClientID     string            `json:\"client_id\"`\n\tDeviceID     string            `json:\"device_id\"`\n\tMeta         map[string]string `json:\"meta\"`\n\tRedirectUri  string            `json:\"redirect_uri\"`\n}\n\ntype CaptchaTokenResponse struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tUrl          string `json:\"url\"`\n}\n\n/*\n* 登录\n**/\ntype TokenResp struct {\n\tTokenType    string `json:\"token_type\"`\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\n\tSub    string `json:\"sub\"`\n\tUserID string `json:\"user_id\"`\n\n\tToken string `json:\"token\"` // \"超级保险箱\" 访问Token\n}\n\nfunc (t *TokenResp) GetToken() string {\n\treturn fmt.Sprint(t.TokenType, \" \", t.AccessToken)\n}\n\n// GetSpaceToken 获取\"超级保险箱\" 访问Token\nfunc (t *TokenResp) GetSpaceToken() string {\n\treturn t.Token\n}\n\ntype SignInRequest struct {\n\tClientID     string `json:\"client_id\"`\n\tClientSecret string `json:\"client_secret\"`\n\n\tProvider    string `json:\"provider\"`\n\tSigninToken string `json:\"signin_token\"`\n}\ntype CoreLoginRequest struct {\n\tProtocolVersion string `json:\"protocolVersion\"`\n\tSequenceNo      string `json:\"sequenceNo\"`\n\tPlatformVersion string `json:\"platformVersion\"`\n\tIsCompressed    string `json:\"isCompressed\"`\n\tAppid           string `json:\"appid\"`\n\tClientVersion   string `json:\"clientVersion\"`\n\tPeerID          string `json:\"peerID\"`\n\tAppName         string `json:\"appName\"`\n\tSdkVersion      string `json:\"sdkVersion\"`\n\tDevicesign      string `json:\"devicesign\"`\n\tNetWorkType     string `json:\"netWorkType\"`\n\tProviderName    string `json:\"providerName\"`\n\tDeviceModel     string `json:\"deviceModel\"`\n\tDeviceName      string `json:\"deviceName\"`\n\tOSVersion       string `json:\"OSVersion\"`\n\tCreditkey       string `json:\"creditkey\"`\n\tHl              string `json:\"hl\"`\n\tUserName        string `json:\"userName\"`\n\tPassWord        string `json:\"passWord\"`\n\tVerifyKey       string `json:\"verifyKey\"`\n\tVerifyCode      string `json:\"verifyCode\"`\n\tIsMd5Pwd        string `json:\"isMd5Pwd\"`\n}\n\ntype CoreLoginResp struct {\n\tAccount   string `json:\"account\"`\n\tCreditkey string `json:\"creditkey\"`\n\t/*\tError              string `json:\"error\"`\n\t\tErrorCode          string `json:\"errorCode\"`\n\t\tErrorDescription   string `json:\"error_description\"`*/\n\tExpiresIn          int    `json:\"expires_in\"`\n\tIsCompressed       string `json:\"isCompressed\"`\n\tIsSetPassWord      string `json:\"isSetPassWord\"`\n\tKeepAliveMinPeriod string `json:\"keepAliveMinPeriod\"`\n\tKeepAlivePeriod    string `json:\"keepAlivePeriod\"`\n\tLoginKey           string `json:\"loginKey\"`\n\tNickName           string `json:\"nickName\"`\n\tPlatformVersion    string `json:\"platformVersion\"`\n\tProtocolVersion    string `json:\"protocolVersion\"`\n\tSecureKey          string `json:\"secureKey\"`\n\tSequenceNo         string `json:\"sequenceNo\"`\n\tSessionID          string `json:\"sessionID\"`\n\tTimestamp          string `json:\"timestamp\"`\n\tUserID             string `json:\"userID\"`\n\tUserName           string `json:\"userName\"`\n\tUserNewNo          string `json:\"userNewNo\"`\n\tVersion            string `json:\"version\"`\n\t/*\tVipList []struct {\n\t\tExpireDate string `json:\"expireDate\"`\n\t\tIsAutoDeduct string `json:\"isAutoDeduct\"`\n\t\tIsVip string `json:\"isVip\"`\n\t\tIsYear string `json:\"isYear\"`\n\t\tPayID string `json:\"payId\"`\n\t\tPayName string `json:\"payName\"`\n\t\tRegister string `json:\"register\"`\n\t\tVasid string `json:\"vasid\"`\n\t\tVasType string `json:\"vasType\"`\n\t\tVipDayGrow string `json:\"vipDayGrow\"`\n\t\tVipGrow string `json:\"vipGrow\"`\n\t\tVipLevel string `json:\"vipLevel\"`\n\t\tIcon struct {\n\t\t\tGeneral string `json:\"general\"`\n\t\t\tSmall string `json:\"small\"`\n\t\t} `json:\"icon\"`\n\t} `json:\"vipList\"`*/\n}\n\n/*\n* 文件\n**/\ntype FileList struct {\n\tKind            string  `json:\"kind\"`\n\tNextPageToken   string  `json:\"next_page_token\"`\n\tFiles           []Files `json:\"files\"`\n\tVersion         string  `json:\"version\"`\n\tVersionOutdated bool    `json:\"version_outdated\"`\n\tFolderType      int8\n}\n\ntype Link struct {\n\tURL    string    `json:\"url\"`\n\tToken  string    `json:\"token\"`\n\tExpire time.Time `json:\"expire\"`\n\tType   string    `json:\"type\"`\n}\n\nvar _ model.Obj = (*Files)(nil)\n\ntype Files struct {\n\tKind     string `json:\"kind\"`\n\tID       string `json:\"id\"`\n\tParentID string `json:\"parent_id\"`\n\tName     string `json:\"name\"`\n\t//UserID         string    `json:\"user_id\"`\n\tSize string `json:\"size\"`\n\t//Revision       string    `json:\"revision\"`\n\t//FileExtension  string    `json:\"file_extension\"`\n\t//MimeType       string    `json:\"mime_type\"`\n\t//Starred        bool      `json:\"starred\"`\n\tWebContentLink string     `json:\"web_content_link\"`\n\tCreatedTime    CustomTime `json:\"created_time\"`\n\tModifiedTime   CustomTime `json:\"modified_time\"`\n\tIconLink       string     `json:\"icon_link\"`\n\tThumbnailLink  string     `json:\"thumbnail_link\"`\n\tMd5Checksum    string     `json:\"md5_checksum\"`\n\tHash           string     `json:\"hash\"`\n\t// Links map[string]Link `json:\"links\"`\n\t// Phase string          `json:\"phase\"`\n\t// Audit struct {\n\t// \tStatus  string `json:\"status\"`\n\t// \tMessage string `json:\"message\"`\n\t// \tTitle   string `json:\"title\"`\n\t// } `json:\"audit\"`\n\tMedias []struct {\n\t\t//Category       string `json:\"category\"`\n\t\t//IconLink       string `json:\"icon_link\"`\n\t\t//IsDefault      bool   `json:\"is_default\"`\n\t\t//IsOrigin       bool   `json:\"is_origin\"`\n\t\t//IsVisible      bool   `json:\"is_visible\"`\n\t\tLink Link `json:\"link\"`\n\t\t//MediaID        string `json:\"media_id\"`\n\t\t//MediaName      string `json:\"media_name\"`\n\t\t//NeedMoreQuota  bool   `json:\"need_more_quota\"`\n\t\t//Priority       int    `json:\"priority\"`\n\t\t//RedirectLink   string `json:\"redirect_link\"`\n\t\t//ResolutionName string `json:\"resolution_name\"`\n\t\t// Video          struct {\n\t\t// \tAudioCodec string `json:\"audio_codec\"`\n\t\t// \tBitRate    int    `json:\"bit_rate\"`\n\t\t// \tDuration   int    `json:\"duration\"`\n\t\t// \tFrameRate  int    `json:\"frame_rate\"`\n\t\t// \tHeight     int    `json:\"height\"`\n\t\t// \tVideoCodec string `json:\"video_codec\"`\n\t\t// \tVideoType  string `json:\"video_type\"`\n\t\t// \tWidth      int    `json:\"width\"`\n\t\t// } `json:\"video\"`\n\t\t// VipTypes []string `json:\"vip_types\"`\n\t} `json:\"medias\"`\n\tTrashed     bool   `json:\"trashed\"`\n\tDeleteTime  string `json:\"delete_time\"`\n\tOriginalURL string `json:\"original_url\"`\n\t//Params            struct{} `json:\"params\"`\n\t//OriginalFileIndex int    `json:\"original_file_index\"`\n\tSpace string `json:\"space\"`\n\t//Apps              []interface{} `json:\"apps\"`\n\t//Writable   bool   `json:\"writable\"`\n\tFolderType string `json:\"folder_type\"`\n\t//Collection interface{} `json:\"collection\"`\n\tSortName         string     `json:\"sort_name\"`\n\tUserModifiedTime CustomTime `json:\"user_modified_time\"`\n\t//SpellName         []interface{} `json:\"spell_name\"`\n\t//FileCategory      string        `json:\"file_category\"`\n\t//Tags              []interface{} `json:\"tags\"`\n\t//ReferenceEvents   []interface{} `json:\"reference_events\"`\n\t//ReferenceResource interface{}   `json:\"reference_resource\"`\n\t//Params0           struct {\n\t//\tPlatformIcon   string `json:\"platform_icon\"`\n\t//\tSmallThumbnail string `json:\"small_thumbnail\"`\n\t//} `json:\"params,omitempty\"`\n}\n\nfunc (c *Files) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(hash_extend.GCID, c.Hash)\n}\n\nfunc (c *Files) GetSize() int64        { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }\nfunc (c *Files) GetName() string       { return c.Name }\nfunc (c *Files) CreateTime() time.Time { return c.CreatedTime.Time }\nfunc (c *Files) ModTime() time.Time    { return c.ModifiedTime.Time }\nfunc (c *Files) IsDir() bool           { return c.Kind == FOLDER }\nfunc (c *Files) GetID() string         { return c.ID }\nfunc (c *Files) GetPath() string {\n\treturn \"\"\n}\nfunc (c *Files) Thumb() string { return c.ThumbnailLink }\n\nfunc (c *Files) GetSpace() string {\n\tif c.Space != \"\" {\n\t\treturn c.Space\n\t} else {\n\t\t// \"迅雷云盘\" 文件夹内 Space 为空\n\t\treturn \"\"\n\t}\n}\n\n/*\n* 上传\n**/\ntype UploadTaskResponse struct {\n\tUploadType string `json:\"upload_type\"`\n\n\t/*//UPLOAD_TYPE_FORM\n\tForm struct {\n\t\t//Headers struct{} `json:\"headers\"`\n\t\tKind       string `json:\"kind\"`\n\t\tMethod     string `json:\"method\"`\n\t\tMultiParts struct {\n\t\t\tOSSAccessKeyID string `json:\"OSSAccessKeyId\"`\n\t\t\tSignature      string `json:\"Signature\"`\n\t\t\tCallback       string `json:\"callback\"`\n\t\t\tKey            string `json:\"key\"`\n\t\t\tPolicy         string `json:\"policy\"`\n\t\t\tXUserData      string `json:\"x:user_data\"`\n\t\t} `json:\"multi_parts\"`\n\t\tURL string `json:\"url\"`\n\t} `json:\"form\"`*/\n\n\t//UPLOAD_TYPE_RESUMABLE\n\tResumable struct {\n\t\tKind   string `json:\"kind\"`\n\t\tParams struct {\n\t\t\tAccessKeyID     string    `json:\"access_key_id\"`\n\t\t\tAccessKeySecret string    `json:\"access_key_secret\"`\n\t\t\tBucket          string    `json:\"bucket\"`\n\t\t\tEndpoint        string    `json:\"endpoint\"`\n\t\t\tExpiration      time.Time `json:\"expiration\"`\n\t\t\tKey             string    `json:\"key\"`\n\t\t\tSecurityToken   string    `json:\"security_token\"`\n\t\t} `json:\"params\"`\n\t\tProvider string `json:\"provider\"`\n\t} `json:\"resumable\"`\n\n\tFile Files `json:\"file\"`\n}\n\n// OfflineDownloadResp 离线下载响应\ntype OfflineDownloadResp struct {\n\tFile       *string     `json:\"file\"`\n\tTask       OfflineTask `json:\"task\"`\n\tUploadType string      `json:\"upload_type\"`\n\tURL        struct {\n\t\tKind string `json:\"kind\"`\n\t} `json:\"url\"`\n}\n\n// OfflineListResp 离线下载列表响应\ntype OfflineListResp struct {\n\tExpiresIn     int64         `json:\"expires_in\"`\n\tNextPageToken string        `json:\"next_page_token\"`\n\tTasks         []OfflineTask `json:\"tasks\"`\n}\n\n// OfflineTask 离线下载任务响应\ntype OfflineTask struct {\n\tCallback    string   `json:\"callback\"`\n\tCreatedTime string   `json:\"created_time\"`\n\tFileID      string   `json:\"file_id\"`\n\tFileName    string   `json:\"file_name\"`\n\tFileSize    string   `json:\"file_size\"`\n\tIconLink    string   `json:\"icon_link\"`\n\tID          string   `json:\"id\"`\n\tKind        string   `json:\"kind\"`\n\tMessage     string   `json:\"message\"`\n\tName        string   `json:\"name\"`\n\tParams      Params   `json:\"params\"`\n\tPhase       string   `json:\"phase\"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING\n\tProgress    int64    `json:\"progress\"`\n\tSpace       string   `json:\"space\"`\n\tStatusSize  int64    `json:\"status_size\"`\n\tStatuses    []string `json:\"statuses\"`\n\tThirdTaskID string   `json:\"third_task_id\"`\n\tType        string   `json:\"type\"`\n\tUpdatedTime string   `json:\"updated_time\"`\n\tUserID      string   `json:\"user_id\"`\n}\n\ntype Params struct {\n\tFolderType   string `json:\"folder_type\"`\n\tPredictSpeed string `json:\"predict_speed\"`\n\tPredictType  string `json:\"predict_type\"`\n}\n\n// LoginReviewResp 登录验证响应\ntype LoginReviewResp struct {\n\tCreditkey        string `json:\"creditkey\"`\n\tError            string `json:\"error\"`\n\tErrorCode        string `json:\"errorCode\"`\n\tErrorDesc        string `json:\"errorDesc\"`\n\tErrorDescURL     string `json:\"errorDescUrl\"`\n\tErrorIsRetry     int    `json:\"errorIsRetry\"`\n\tErrorDescription string `json:\"error_description\"`\n\tIsCompressed     string `json:\"isCompressed\"`\n\tPlatformVersion  string `json:\"platformVersion\"`\n\tProtocolVersion  string `json:\"protocolVersion\"`\n\tReviewurl        string `json:\"reviewurl\"`\n\tSequenceNo       string `json:\"sequenceNo\"`\n\tUserID           string `json:\"userID\"`\n\tVerifyType       string `json:\"verifyType\"`\n}\n\n// ReviewData 验证数据\ntype ReviewData struct {\n\tCreditkey  string `json:\"creditkey\"`\n\tReviewurl  string `json:\"reviewurl\"`\n\tDeviceid   string `json:\"deviceid\"`\n\tDevicesign string `json:\"devicesign\"`\n}\n\ntype AboutResponse struct {\n\tQuota struct {\n\t\tLimit string `json:\"limit\"`\n\t\tUsage string `json:\"usage\"`\n\t} `json:\"quota\"`\n}\n"
  },
  {
    "path": "drivers/thunder_browser/util.go",
    "content": "package thunder_browser\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tAPI_URL             = \"https://x-api-pan.xunlei.com/drive/v1\"\n\tFILE_API_URL        = API_URL + \"/files\"\n\tTASK_API_URL        = API_URL + \"/tasks\"\n\tXLUSER_API_BASE_URL = \"https://xluser-ssl.xunlei.com\"\n\tXLUSER_API_URL      = XLUSER_API_BASE_URL + \"/v1\"\n)\n\nvar Algorithms = []string{\n\t\"Cw4kArmKJ/aOiFTxnQ0ES+D4mbbrIUsFn\",\n\t\"HIGg0Qfbpm5ThZ/RJfjoao4YwgT9/M\",\n\t\"u/PUD\",\n\t\"OlAm8tPkOF1qO5bXxRN2iFttuDldrg\",\n\t\"FFIiM6sFhWhU7tIMVUKOF7CUv/KzgwwV8FE\",\n\t\"yN\",\n\t\"4m5mglrIHksI6wYdq\",\n\t\"LXEfS7\",\n\t\"T+p+C+F2yjgsUtiXWU/cMNYEtJI4pq7GofW\",\n\t\"14BrGIEMXkbvFvZ49nDUfVCRcHYFOJ1BP1Y\",\n\t\"kWIH3Row\",\n\t\"RAmRTKNCjucPWC\",\n}\n\nconst (\n\tClientID          = \"ZUBzD9J_XPXfn7f7\"\n\tClientSecret      = \"yESVmHecEe6F0aou69vl-g\"\n\tClientVersion     = \"1.40.0.7208\"\n\tPackageName       = \"com.xunlei.browser\"\n\tDownloadUserAgent = \"AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)\"\n\tSdkVersion        = \"509300\"\n)\n\nconst (\n\tFOLDER    = \"drive#folder\"\n\tFILE      = \"drive#file\"\n\tRESUMABLE = \"drive#resumable\"\n)\n\nconst (\n\tUPLOAD_TYPE_UNKNOWN = \"UPLOAD_TYPE_UNKNOWN\"\n\t//UPLOAD_TYPE_FORM      = \"UPLOAD_TYPE_FORM\"\n\tUPLOAD_TYPE_RESUMABLE = \"UPLOAD_TYPE_RESUMABLE\"\n\tUPLOAD_TYPE_URL       = \"UPLOAD_TYPE_URL\"\n)\n\nconst (\n\tThunderDriveSpace                       = \"\"\n\tThunderDriveSafeSpace                   = \"SPACE_SAFE\"\n\tThunderBrowserDriveSpace                = \"SPACE_BROWSER\"\n\tThunderBrowserDriveSafeSpace            = \"SPACE_BROWSER_SAFE\"\n\tThunderDriveFolderType                  = \"DEFAULT_ROOT\"\n\tThunderBrowserDriveSafeFolderType       = \"BROWSER_SAFE\"\n\tThunderBrowserDriveFluentPlayFolderType = \"SPACE_FAVORITE\" // 流畅播文件夹标识\n)\n\nconst (\n\tSignProvider = \"access_end_point_token\"\n\tAPPID        = \"22062\"\n\tAPPKey       = \"a5d7416858147a4ab99573872ffccef8\"\n)\n\nfunc GetAction(method string, url string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(url)[1]\n\treturn method + \":\" + urlpath\n}\n\ntype Common struct {\n\tclient *resty.Client\n\n\tcaptchaToken string\n\n\tcreditKey string\n\n\t// 签名相关,二选一\n\tAlgorithms             []string\n\tTimestamp, CaptchaSign string\n\n\t// 必要值,签名相关\n\tDeviceID          string\n\tClientID          string\n\tClientSecret      string\n\tClientVersion     string\n\tPackageName       string\n\tUserAgent         string\n\tDownloadUserAgent string\n\tUseVideoUrl       bool\n\tUseFluentPlay     bool\n\tRemoveWay         string\n\n\t// 验证码token刷新成功回调\n\trefreshCTokenCk func(token string)\n}\n\nfunc (c *Common) SetDeviceID(deviceID string) {\n\tc.DeviceID = deviceID\n}\n\nfunc (c *Common) SetCaptchaToken(captchaToken string) {\n\tc.captchaToken = captchaToken\n}\nfunc (c *Common) GetCaptchaToken() string {\n\treturn c.captchaToken\n}\n\nfunc (c *Common) SetCreditKey(creditKey string) {\n\tc.creditKey = creditKey\n}\nfunc (c *Common) GetCreditKey() string {\n\treturn c.creditKey\n}\n\n// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)\nfunc (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {\n\tmetas := map[string]string{\n\t\t\"client_version\": c.ClientVersion,\n\t\t\"package_name\":   c.PackageName,\n\t\t\"user_id\":        userID,\n\t}\n\tmetas[\"timestamp\"], metas[\"captcha_sign\"] = c.GetCaptchaSign()\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)\nfunc (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {\n\tmetas := make(map[string]string)\n\tif ok, _ := regexp.MatchString(`\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*`, username); ok {\n\t\tmetas[\"email\"] = username\n\t} else if len(username) >= 11 && len(username) <= 18 {\n\t\tmetas[\"phone_number\"] = username\n\t} else {\n\t\tmetas[\"username\"] = username\n\t}\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// GetCaptchaSign 获取验证码签名\nfunc (c *Common) GetCaptchaSign() (timestamp, sign string) {\n\tif len(c.Algorithms) == 0 {\n\t\treturn c.Timestamp, c.CaptchaSign\n\t}\n\ttimestamp = fmt.Sprint(time.Now().UnixMilli())\n\tstr := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)\n\tfor _, algorithm := range c.Algorithms {\n\t\tstr = utils.GetMD5EncodeStr(str + algorithm)\n\t}\n\tsign = \"1.\" + str\n\treturn\n}\n\n// 刷新验证码token\nfunc (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {\n\tparam := CaptchaTokenRequest{\n\t\tAction:       action,\n\t\tCaptchaToken: c.captchaToken,\n\t\tClientID:     c.ClientID,\n\t\tDeviceID:     c.DeviceID,\n\t\tMeta:         metas,\n\t\tRedirectUri:  \"xlaccsdk01://xunlei.com/callback?state=harbor\",\n\t}\n\tvar e ErrResp\n\tvar resp CaptchaTokenResponse\n\t_, err := c.Request(XLUSER_API_URL+\"/shield/captcha/init\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetError(&e).SetBody(param)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif e.IsError() {\n\t\treturn &e\n\t}\n\n\tif resp.Url != \"\" {\n\t\treturn fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, resp.Url)\n\t}\n\n\tif resp.CaptchaToken == \"\" {\n\t\treturn fmt.Errorf(\"empty captchaToken\")\n\t}\n\n\tif c.refreshCTokenCk != nil {\n\t\tc.refreshCTokenCk(resp.CaptchaToken)\n\t}\n\tc.SetCaptchaToken(resp.CaptchaToken)\n\treturn nil\n}\n\n// Request 只有基础信息的请求\nfunc (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := c.client.R().SetHeaders(map[string]string{\n\t\t\"user-agent\":       c.UserAgent,\n\t\t\"accept\":           \"application/json;charset=UTF-8\",\n\t\t\"x-device-id\":      c.DeviceID,\n\t\t\"x-client-id\":      c.ClientID,\n\t\t\"x-client-version\": c.ClientVersion,\n\t})\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar erron ErrResp\n\tutils.Json.Unmarshal(res.Body(), &erron)\n\tif erron.IsError() {\n\t\t// review_panel 表示需要短信验证码进行验证\n\t\tif erron.ErrorMsg == \"review_panel\" {\n\t\t\treturn nil, c.getReviewData(res)\n\t\t}\n\n\t\treturn nil, &erron\n\t}\n\n\treturn res.Body(), nil\n}\n\n// 获取验证所需内容\nfunc (c *Common) getReviewData(res *resty.Response) error {\n\tvar reviewResp LoginReviewResp\n\tvar reviewData ReviewData\n\n\tif err := utils.Json.Unmarshal(res.Body(), &reviewResp); err != nil {\n\t\treturn err\n\t}\n\n\tdeviceSign := generateDeviceSign(c.DeviceID, c.PackageName)\n\n\treviewData = ReviewData{\n\t\tCreditkey:  reviewResp.Creditkey,\n\t\tReviewurl:  reviewResp.Reviewurl + \"&deviceid=\" + deviceSign,\n\t\tDeviceid:   deviceSign,\n\t\tDevicesign: deviceSign,\n\t}\n\n\t// 将reviewData转为JSON字符串\n\treviewDataJSON, _ := json.MarshalIndent(reviewData, \"\", \"  \")\n\t//reviewDataJSON, _ := json.Marshal(reviewData)\n\n\treturn fmt.Errorf(`\n<div style=\"font-family: Arial, sans-serif; padding: 15px; border-radius: 5px; border: 1px solid #e0e0e0;>\n    <h3 style=\"color: #d9534f; margin-top: 0;\">\n        <span style=\"font-size: 16px;\">🔒 本次登录需要验证</span><br>\n        <span style=\"font-size: 14px; font-weight: normal; color: #666;\">This login requires verification</span>\n    </h3>\n    <p style=\"font-size: 14px; margin-bottom: 15px;\">下面是验证所需要的数据，具体使用方法请参照对应的驱动文档<br>\n    <span style=\"color: #666; font-size: 13px;\">Below are the relevant verification data. For specific usage methods, please refer to the corresponding driver documentation.</span></p>\n    <div style=\"border: 1px solid #ddd; border-radius: 4px; padding: 10px; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 13px;\">\n        <pre style=\"margin: 0; white-space: pre-wrap;\"><code>%s</code></pre>\n    </div>\n</div>`, string(reviewDataJSON))\n}\n\n// 计算文件Gcid\nfunc getGcid(r io.Reader, size int64) (string, error) {\n\tcalcBlockSize := func(j int64) int64 {\n\t\tvar psize int64 = 0x40000\n\t\tfor float64(j)/float64(psize) > 0x200 && psize < 0x200000 {\n\t\t\tpsize = psize << 1\n\t\t}\n\t\treturn psize\n\t}\n\n\thash1 := sha1.New()\n\thash2 := sha1.New()\n\treadSize := calcBlockSize(size)\n\tfor {\n\t\thash2.Reset()\n\t\tif n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {\n\t\t\tif err != io.EOF {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\thash1.Write(hash2.Sum(nil))\n\t}\n\treturn hex.EncodeToString(hash1.Sum(nil)), nil\n}\n\ntype CustomTime struct {\n\ttime.Time\n}\n\nconst timeFormat = time.RFC3339\n\nfunc (ct *CustomTime) UnmarshalJSON(b []byte) error {\n\tstr := string(b)\n\tif str == `\"\"` {\n\t\t*ct = CustomTime{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}\n\t\treturn nil\n\t}\n\n\tt, err := time.Parse(`\"`+timeFormat+`\"`, str)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*ct = CustomTime{Time: t}\n\treturn nil\n}\n\n// EncryptPassword 超级保险箱 加密\nfunc EncryptPassword(password string) string {\n\tif password == \"\" {\n\t\treturn \"\"\n\t}\n\t// 将字符串转换为字节数组\n\tbyteData := []byte(password)\n\t// 计算MD5哈希值\n\thash := md5.Sum(byteData)\n\t// 将哈希值转换为十六进制字符串\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc generateDeviceSign(deviceID, packageName string) string {\n\n\tsignatureBase := fmt.Sprintf(\"%s%s%s%s\", deviceID, packageName, APPID, APPKey)\n\n\tsha1Hash := sha1.New()\n\tsha1Hash.Write([]byte(signatureBase))\n\tsha1Result := sha1Hash.Sum(nil)\n\n\tsha1String := hex.EncodeToString(sha1Result)\n\n\tmd5Hash := md5.New()\n\tmd5Hash.Write([]byte(sha1String))\n\tmd5Result := md5Hash.Sum(nil)\n\n\tmd5String := hex.EncodeToString(md5Result)\n\n\tdeviceSign := fmt.Sprintf(\"div101.%s%s\", deviceID, md5String)\n\n\treturn deviceSign\n}\n\nfunc BuildCustomUserAgent(deviceID, appName, sdkVersion, clientVersion, packageName string) string {\n\t//deviceSign := generateDeviceSign(deviceID, packageName)\n\tvar sb strings.Builder\n\n\tsb.WriteString(fmt.Sprintf(\"ANDROID-%s/%s \", appName, clientVersion))\n\tsb.WriteString(\"networkType/WIFI \")\n\tsb.WriteString(fmt.Sprintf(\"appid/%s \", APPID))\n\tsb.WriteString(fmt.Sprintf(\"deviceName/Xiaomi_M2004j7ac \"))\n\tsb.WriteString(fmt.Sprintf(\"deviceModel/M2004J7AC \"))\n\tsb.WriteString(fmt.Sprintf(\"OSVersion/13 \"))\n\tsb.WriteString(fmt.Sprintf(\"protocolVersion/301 \"))\n\tsb.WriteString(fmt.Sprintf(\"platformversion/10 \"))\n\tsb.WriteString(fmt.Sprintf(\"sdkVersion/%s \", sdkVersion))\n\tsb.WriteString(fmt.Sprintf(\"Oauth2Client/0.9 (Linux 4_9_337-perf-sn-uotan-gd9d488809c3d) (JAVA 0) \"))\n\treturn sb.String()\n}\n"
  },
  {
    "path": "drivers/thunderx/driver.go",
    "content": "package thunderx\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype ThunderX struct {\n\t*XunLeiXCommon\n\tmodel.Storage\n\tAddition\n\n\tidentity string\n}\n\nfunc (x *ThunderX) Config() driver.Config {\n\treturn config\n}\n\nfunc (x *ThunderX) GetAddition() driver.Additional {\n\treturn &x.Addition\n}\n\nfunc (x *ThunderX) Init(ctx context.Context) (err error) {\n\t// 初始化所需参数\n\tif x.XunLeiXCommon == nil {\n\t\tx.XunLeiXCommon = &XunLeiXCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient:            base.NewRestyClient(),\n\t\t\t\tAlgorithms:        Algorithms,\n\t\t\t\tDeviceID:          utils.GetMD5EncodeStr(x.Username + x.Password),\n\t\t\t\tClientID:          ClientID,\n\t\t\t\tClientSecret:      ClientSecret,\n\t\t\t\tClientVersion:     ClientVersion,\n\t\t\t\tPackageName:       PackageName,\n\t\t\t\tUserAgent:         BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, \"\"),\n\t\t\t\tDownloadUserAgent: DownloadUserAgent,\n\t\t\t\tUseVideoUrl:       x.UseVideoUrl,\n\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t\trefreshTokenFunc: func() error {\n\t\t\t\t// 通过RefreshToken刷新\n\t\t\t\ttoken, err := x.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// 重新登录\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t\tif token.UserID != \"\" {\n\t\t\t\t\t\t\tx.SetUserID(token.UserID)\n\t\t\t\t\t\t\tx.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)\n\t\t\t\t\t\t}\n\t\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\treturn err\n\t\t\t},\n\t\t}\n\t}\n\n\t// 自定义验证码token\n\tctoken := strings.TrimSpace(x.CaptchaToken)\n\tif ctoken != \"\" {\n\t\tx.SetCaptchaToken(ctoken)\n\t}\n\tif x.DeviceID == \"\" {\n\t\tx.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))\n\t}\n\n\tx.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl\n\tx.Addition.RootFolderID = x.RootFolderID\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif x.identity != identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\t// 登录\n\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tx.SetTokenResp(token)\n\t\tif token.UserID != \"\" {\n\t\t\tx.SetUserID(token.UserID)\n\t\t\tx.UserAgent = BuildCustomUserAgent(x.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ThunderX) Drop(ctx context.Context) error {\n\treturn nil\n}\n\ntype ThunderXExpert struct {\n\t*XunLeiXCommon\n\tmodel.Storage\n\tExpertAddition\n\n\tidentity string\n}\n\nfunc (x *ThunderXExpert) Config() driver.Config {\n\treturn configExpert\n}\n\nfunc (x *ThunderXExpert) GetAddition() driver.Additional {\n\treturn &x.ExpertAddition\n}\n\nfunc (x *ThunderXExpert) Init(ctx context.Context) (err error) {\n\t// 防止重复登录\n\tidentity := x.GetIdentity()\n\tif identity != x.identity || !x.IsLogin() {\n\t\tx.identity = identity\n\t\tx.XunLeiXCommon = &XunLeiXCommon{\n\t\t\tCommon: &Common{\n\t\t\t\tclient: base.NewRestyClient(),\n\n\t\t\t\tDeviceID: func() string {\n\t\t\t\t\tif len(x.DeviceID) != 32 {\n\t\t\t\t\t\tif x.LoginType == \"user\" {\n\t\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.Username + x.Password)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)\n\t\t\t\t\t}\n\t\t\t\t\treturn x.DeviceID\n\t\t\t\t}(),\n\t\t\t\tClientID:      x.ClientID,\n\t\t\t\tClientSecret:  x.ClientSecret,\n\t\t\t\tClientVersion: x.ClientVersion,\n\t\t\t\tPackageName:   x.PackageName,\n\t\t\t\tUserAgent: func() string {\n\t\t\t\t\tif x.ExpertAddition.UserAgent != \"\" {\n\t\t\t\t\t\treturn x.ExpertAddition.UserAgent\n\t\t\t\t\t}\n\t\t\t\t\tif x.LoginType == \"user\" {\n\t\t\t\t\t\treturn BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, \"\")\n\t\t\t\t\t}\n\t\t\t\t\treturn BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, \"\")\n\t\t\t\t}(),\n\t\t\t\tDownloadUserAgent: func() string {\n\t\t\t\t\tif x.ExpertAddition.DownloadUserAgent != \"\" {\n\t\t\t\t\t\treturn x.ExpertAddition.DownloadUserAgent\n\t\t\t\t\t}\n\t\t\t\t\treturn DownloadUserAgent\n\t\t\t\t}(),\n\t\t\t\tUseVideoUrl: x.UseVideoUrl,\n\t\t\t\trefreshCTokenCk: func(token string) {\n\t\t\t\t\tx.CaptchaToken = token\n\t\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tif x.ExpertAddition.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.ExpertAddition.CaptchaToken)\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tif x.Common.DeviceID != \"\" {\n\t\t\tx.ExpertAddition.DeviceID = x.Common.DeviceID\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tif x.Common.DownloadUserAgent != \"\" {\n\t\t\tx.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t\tx.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl\n\t\tx.ExpertAddition.RootFolderID = x.RootFolderID\n\t\t// 签名方法\n\t\tif x.SignType == \"captcha_sign\" {\n\t\t\tx.Common.Timestamp = x.Timestamp\n\t\t\tx.Common.CaptchaSign = x.CaptchaSign\n\t\t} else {\n\t\t\tx.Common.Algorithms = strings.Split(x.Algorithms, \",\")\n\t\t}\n\n\t\t// 登录方式\n\t\tif x.LoginType == \"refresh_token\" {\n\t\t\t// 通过RefreshToken登录\n\t\t\ttoken, err := x.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tx.SetTokenResp(token)\n\t\t\t// 刷新token方法\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\t\t} else {\n\t\t\t// 通过用户密码登录\n\t\t\ttoken, err := x.Login(x.Username, x.Password)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tx.SetTokenResp(token)\n\t\t\tx.SetRefreshTokenFunc(func() error {\n\t\t\t\ttoken, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\ttoken, err = x.Login(x.Username, x.Password)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tx.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tx.SetTokenResp(token)\n\t\t\t\top.MustSaveDriverStorage(x)\n\t\t\t\treturn err\n\t\t\t})\n\t\t}\n\t\t// 更新 UserAgent\n\t\tif x.TokenResp.UserID != \"\" {\n\t\t\tx.ExpertAddition.UserAgent = BuildCustomUserAgent(x.ExpertAddition.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, x.TokenResp.UserID)\n\t\t\tx.SetUserAgent(x.ExpertAddition.UserAgent)\n\t\t\top.MustSaveDriverStorage(x)\n\t\t}\n\t} else {\n\t\t// 仅修改验证码token\n\t\tif x.CaptchaToken != \"\" {\n\t\t\tx.SetCaptchaToken(x.CaptchaToken)\n\t\t}\n\t\tx.XunLeiXCommon.UserAgent = x.ExpertAddition.UserAgent\n\t\tx.XunLeiXCommon.DownloadUserAgent = x.ExpertAddition.UserAgent\n\t\tx.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl\n\t\tx.ExpertAddition.RootFolderID = x.RootFolderID\n\t}\n\treturn nil\n}\n\nfunc (x *ThunderXExpert) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (x *ThunderXExpert) SetTokenResp(token *TokenResp) {\n\tx.XunLeiXCommon.SetTokenResp(token)\n\tif token != nil {\n\t\tx.ExpertAddition.RefreshToken = token.RefreshToken\n\t}\n}\n\ntype XunLeiXCommon struct {\n\t*Common\n\t*TokenResp // 登录信息\n\n\trefreshTokenFunc func() error\n}\n\nfunc (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\treturn xc.getFiles(ctx, dir.GetID())\n}\n\nfunc (xc *XunLeiXCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar lFile Files\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", file.GetID())\n\t\t//r.SetQueryParam(\"space\", \"\")\n\t}, &lFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := &model.Link{\n\t\tURL: lFile.WebContentLink,\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\": {xc.DownloadUserAgent},\n\t\t},\n\t}\n\n\tif xc.UseVideoUrl {\n\t\tfor _, media := range lFile.Medias {\n\t\t\tif media.Link.URL != \"\" {\n\t\t\t\tlink.URL = media.Link.URL\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t\tstrs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)\n\t\tif len(strs) == 2 {\n\t\t\ttimestamp, err := strconv.ParseInt(strs[1], 10, 64)\n\t\t\tif err == nil {\n\t\t\t\texpired := time.Duration(timestamp-time.Now().Unix()) * time.Second\n\t\t\t\tlink.Expiration = &expired\n\t\t\t}\n\t\t}\n\t*/\n\treturn link, nil\n}\n\nfunc (xc *XunLeiXCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"kind\":      FOLDER,\n\t\t\t\"name\":      dirName,\n\t\t\t\"parent_id\": parentDir.GetID(),\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiXCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\":batchMove\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"to\":  base.Json{\"parent_id\": dstDir.GetID()},\n\t\t\t\"ids\": []string{srcObj.GetID()},\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiXCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}\", http.MethodPatch, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", srcObj.GetID())\n\t\tr.SetBody(&base.Json{\"name\": newName})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiXCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\":batchCopy\", http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"to\":  base.Json{\"parent_id\": dstDir.GetID()},\n\t\t\t\"ids\": []string{srcObj.GetID()},\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiXCommon) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := xc.Request(FILE_API_URL+\"/{fileID}/trash\", http.MethodPatch, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetPathParam(\"fileID\", obj.GetID())\n\t\tr.SetBody(\"{}\")\n\t}, nil)\n\treturn err\n}\n\nfunc (xc *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tgcid := file.GetHash().GetHash(hash_extend.GCID)\n\tvar err error\n\tif len(gcid) < hash_extend.GCID.Width {\n\t\t_, gcid, err = stream.CacheFullAndHash(file, &up, hash_extend.GCID, file.GetSize())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar resp UploadTaskResponse\n\t_, err = xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t\tr.SetBody(&base.Json{\n\t\t\t\"kind\":        FILE,\n\t\t\t\"parent_id\":   dstDir.GetID(),\n\t\t\t\"name\":        file.GetName(),\n\t\t\t\"size\":        file.GetSize(),\n\t\t\t\"hash\":        gcid,\n\t\t\t\"upload_type\": UPLOAD_TYPE_RESUMABLE,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparam := resp.Resumable.Params\n\tif resp.UploadType == UPLOAD_TYPE_RESUMABLE {\n\t\tparam.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+\".\")\n\t\ts, err := session.NewSession(&aws.Config{\n\t\t\tCredentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),\n\t\t\tRegion:      aws.String(\"xunlei\"),\n\t\t\tEndpoint:    aws.String(param.Endpoint),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuploader := s3manager.NewUploader(s)\n\t\tif file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {\n\t\t\tuploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)\n\t\t}\n\t\t_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{\n\t\t\tBucket:  aws.String(param.Bucket),\n\t\t\tKey:     aws.String(param.Key),\n\t\t\tExpires: aws.Time(param.Expiration),\n\t\t\tBody: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\t\t\tReader:         file,\n\t\t\t\tUpdateProgress: up,\n\t\t\t}),\n\t\t})\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (xc *XunLeiXCommon) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tvar about AboutResponse\n\t_, err := xc.Request(API_URL+\"/about\", http.MethodGet, func(r *resty.Request) {\n\t\tr.SetContext(ctx)\n\t}, &about)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttotal, err := strconv.ParseInt(about.Quota.Limit, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tused, err := strconv.ParseInt(about.Quota.Usage, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  used,\n\t\t},\n\t}, nil\n}\n\nfunc (xc *XunLeiXCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {\n\tfiles := make([]model.Obj, 0)\n\tvar pageToken string\n\tfor {\n\t\tvar fileList FileList\n\t\t_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {\n\t\t\tr.SetContext(ctx)\n\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\"space\":      \"\",\n\t\t\t\t\"__type\":     \"drive\",\n\t\t\t\t\"refresh\":    \"true\",\n\t\t\t\t\"__sync\":     \"true\",\n\t\t\t\t\"parent_id\":  folderId,\n\t\t\t\t\"page_token\": pageToken,\n\t\t\t\t\"with_audit\": \"true\",\n\t\t\t\t\"limit\":      \"100\",\n\t\t\t\t\"filters\":    `{\"phase\":{\"eq\":\"PHASE_TYPE_COMPLETE\"},\"trashed\":{\"eq\":false}}`,\n\t\t\t})\n\t\t}, &fileList)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := 0; i < len(fileList.Files); i++ {\n\t\t\tfiles = append(files, &fileList.Files[i])\n\t\t}\n\n\t\tif fileList.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tpageToken = fileList.NextPageToken\n\t}\n\treturn files, nil\n}\n\n// SetRefreshTokenFunc 设置刷新Token的方法\nfunc (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) {\n\txc.refreshTokenFunc = fn\n}\n\n// SetTokenResp 设置Token\nfunc (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) {\n\txc.TokenResp = tr\n}\n\n// Request 携带Authorization和CaptchaToken的请求\nfunc (xc *XunLeiXCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tdata, err := xc.Common.Request(url, method, func(req *resty.Request) {\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Authorization\":   xc.Token(),\n\t\t\t\"X-Captcha-Token\": xc.GetCaptchaToken(),\n\t\t})\n\t\tif callback != nil {\n\t\t\tcallback(req)\n\t\t}\n\t}, resp)\n\n\tvar errResp *ErrResp\n\tok := errors.As(err, &errResp)\n\tif !ok {\n\t\treturn nil, err\n\t}\n\n\tswitch errResp.ErrorCode {\n\tcase 0:\n\t\treturn data, nil\n\tcase 4122, 4121, 10, 16:\n\t\tif xc.refreshTokenFunc != nil {\n\t\t\tif err = xc.refreshTokenFunc(); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\tcase 9: // 验证码token过期\n\t\tif err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, err\n\t}\n\treturn xc.Request(url, method, callback, resp)\n}\n\n// RefreshToken 刷新Token\nfunc (xc *XunLeiXCommon) RefreshToken(refreshToken string) (*TokenResp, error) {\n\tvar resp TokenResp\n\t_, err := xc.Common.Request(XLUSER_API_URL+\"/auth/token\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(&base.Json{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": refreshToken,\n\t\t\t\"client_id\":     xc.ClientID,\n\t\t\t\"client_secret\": xc.ClientSecret,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.RefreshToken == \"\" {\n\t\treturn nil, errs.EmptyToken\n\t}\n\tresp.UserID = resp.Sub\n\treturn &resp, nil\n}\n\n// Login 登录\nfunc (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) {\n\turl := XLUSER_API_URL + \"/auth/signin\"\n\terr := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp TokenResp\n\t_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(&SignInRequest{\n\t\t\tCaptchaToken: xc.GetCaptchaToken(),\n\t\t\tClientID:     xc.ClientID,\n\t\t\tClientSecret: xc.ClientSecret,\n\t\t\tUsername:     username,\n\t\t\tPassword:     password,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.UserID = resp.Sub\n\treturn &resp, nil\n}\n\nfunc (xc *XunLeiXCommon) IsLogin() bool {\n\tif xc.TokenResp == nil {\n\t\treturn false\n\t}\n\t_, err := xc.Request(XLUSER_API_URL+\"/user/me\", http.MethodGet, nil, nil)\n\treturn err == nil\n}\n\n// 离线下载文件，都和Pikpak接口一致\nfunc (xc *XunLeiXCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {\n\trequestBody := base.Json{\n\t\t\"kind\":        \"drive#file\",\n\t\t\"name\":        fileName,\n\t\t\"upload_type\": \"UPLOAD_TYPE_URL\",\n\t\t\"url\": base.Json{\n\t\t\t\"url\": fileUrl,\n\t\t},\n\t\t\"params\":    base.Json{},\n\t\t\"parent_id\": parentDir.GetID(),\n\t}\n\tvar resp OfflineDownloadResp // 一样的\n\t_, err := xc.Request(FILE_API_URL, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetBody(requestBody)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Task, err\n}\n\n// 获取离线下载任务列表\nfunc (xc *XunLeiXCommon) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {\n\tres := make([]OfflineTask, 0)\n\tif len(phase) == 0 {\n\t\tphase = []string{\"PHASE_TYPE_RUNNING\", \"PHASE_TYPE_ERROR\", \"PHASE_TYPE_COMPLETE\", \"PHASE_TYPE_PENDING\"}\n\t}\n\tparams := map[string]string{\n\t\t\"type\":           \"offline\",\n\t\t\"thumbnail_size\": \"SIZE_SMALL\",\n\t\t\"limit\":          \"10000\",\n\t\t\"page_token\":     nextPageToken,\n\t\t\"with\":           \"reference_resource\",\n\t}\n\n\t// 处理 phase 参数\n\tif len(phase) > 0 {\n\t\tfilters := base.Json{\n\t\t\t\"phase\": map[string]string{\n\t\t\t\t\"in\": strings.Join(phase, \",\"),\n\t\t\t},\n\t\t}\n\t\tfiltersJSON, err := json.Marshal(filters)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to marshal filters: %w\", err)\n\t\t}\n\t\tparams[\"filters\"] = string(filtersJSON)\n\t}\n\n\tvar resp OfflineListResp\n\t_, err := xc.Request(TASKS_API_URL, http.MethodGet, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(params)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get offline list: %w\", err)\n\t}\n\tres = append(res, resp.Tasks...)\n\treturn res, nil\n}\n\nfunc (xc *XunLeiXCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {\n\tparams := map[string]string{\n\t\t\"task_ids\":     strings.Join(taskIDs, \",\"),\n\t\t\"delete_files\": strconv.FormatBool(deleteFiles),\n\t}\n\t_, err := xc.Request(TASKS_API_URL, http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetContext(ctx).\n\t\t\tSetQueryParams(params)\n\t}, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete tasks %v: %w\", taskIDs, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/thunderx/meta.go",
    "content": "package thunderx\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// 高级设置\ntype ExpertAddition struct {\n\tdriver.RootID\n\n\tLoginType string `json:\"login_type\" type:\"select\" options:\"user,refresh_token\" default:\"user\"`\n\tSignType  string `json:\"sign_type\" type:\"select\" options:\"algorithms,captcha_sign\" default:\"algorithms\"`\n\n\t// 登录方式1\n\tUsername string `json:\"username\" required:\"true\" help:\"login type is user,this is required\"`\n\tPassword string `json:\"password\" required:\"true\" help:\"login type is user,this is required\"`\n\t// 登录方式2\n\tRefreshToken string `json:\"refresh_token\" required:\"true\" help:\"login type is refresh_token,this is required\"`\n\n\t// 签名方法1\n\tAlgorithms string `json:\"algorithms\" required:\"true\" help:\"sign type is algorithms,this is required\" default:\"kVy0WbPhiE4v6oxXZ88DvoA3Q,lON/AUoZKj8/nBtcE85mVbkOaVdVa,rLGffQrfBKH0BgwQ33yZofvO3Or,FO6HWqw,GbgvyA2,L1NU9QvIQIH7DTRt,y7llk4Y8WfYflt6,iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe,8C28RTXmVcco0,X5Xh,7xe25YUgfGgD0xW3ezFS,,CKCR,8EmDjBo6h3eLaK7U6vU2Qys0NsMx,t2TeZBXKqbdP09Arh9C3\"`\n\t// 签名方法2\n\tCaptchaSign string `json:\"captcha_sign\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\tTimestamp   string `json:\"timestamp\" required:\"true\" help:\"sign type is captcha_sign,this is required\"`\n\n\t// 验证码\n\tCaptchaToken string `json:\"captcha_token\"`\n\n\t// 必要且影响登录,由签名决定\n\tDeviceID      string `json:\"device_id\"  required:\"false\" default:\"\"`\n\tClientID      string `json:\"client_id\"  required:\"true\" default:\"ZQL_zwA4qhHcoe_2\"`\n\tClientSecret  string `json:\"client_secret\"  required:\"true\" default:\"Og9Vr1L8Ee6bh0olFxFDRg\"`\n\tClientVersion string `json:\"client_version\"  required:\"true\" default:\"1.06.0.2132\"`\n\tPackageName   string `json:\"package_name\"  required:\"true\" default:\"com.thunder.downloader\"`\n\n\t////不影响登录,影响下载速度\n\tUserAgent         string `json:\"user_agent\"  required:\"false\" default:\"\"`\n\tDownloadUserAgent string `json:\"download_user_agent\"  required:\"false\" default:\"\"`\n\n\t//优先使用视频链接代替下载链接\n\tUseVideoUrl bool `json:\"use_video_url\"`\n}\n\n// 登录特征,用于判断是否重新登录\nfunc (i *ExpertAddition) GetIdentity() string {\n\thash := md5.New()\n\tif i.LoginType == \"refresh_token\" {\n\t\thash.Write([]byte(i.RefreshToken))\n\t} else {\n\t\thash.Write([]byte(i.Username + i.Password))\n\t}\n\n\tif i.SignType == \"captcha_sign\" {\n\t\thash.Write([]byte(i.CaptchaSign + i.Timestamp))\n\t} else {\n\t\thash.Write([]byte(i.Algorithms))\n\t}\n\n\thash.Write([]byte(i.DeviceID))\n\thash.Write([]byte(i.ClientID))\n\thash.Write([]byte(i.ClientSecret))\n\thash.Write([]byte(i.ClientVersion))\n\thash.Write([]byte(i.PackageName))\n\treturn hex.EncodeToString(hash.Sum(nil))\n}\n\ntype Addition struct {\n\tdriver.RootID\n\tUsername     string `json:\"username\" required:\"true\"`\n\tPassword     string `json:\"password\" required:\"true\"`\n\tCaptchaToken string `json:\"captcha_token\"`\n\tUseVideoUrl  bool   `json:\"use_video_url\" default:\"true\"`\n}\n\n// 登录特征,用于判断是否重新登录\nfunc (i *Addition) GetIdentity() string {\n\treturn utils.GetMD5EncodeStr(i.Username + i.Password)\n}\n\nvar config = driver.Config{\n\tName:      \"ThunderX\",\n\tLocalSort: true,\n}\n\nvar configExpert = driver.Config{\n\tName:      \"ThunderXExpert\",\n\tLocalSort: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ThunderX{}\n\t})\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ThunderXExpert{}\n\t})\n}\n"
  },
  {
    "path": "drivers/thunderx/types.go",
    "content": "package thunderx\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\thash_extend \"github.com/OpenListTeam/OpenList/v4/pkg/utils/hash\"\n)\n\ntype ErrResp struct {\n\tErrorCode        int64  `json:\"error_code\"`\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n\t//\tErrorDetails   interface{} `json:\"error_details\"`\n}\n\nfunc (e *ErrResp) IsError() bool {\n\treturn e.ErrorCode != 0 || e.ErrorMsg != \"\" || e.ErrorDescription != \"\"\n}\n\nfunc (e *ErrResp) Error() string {\n\treturn fmt.Sprintf(\"ErrorCode: %d ,Error: %s ,ErrorDescription: %s \", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)\n}\n\n/*\n* 验证码Token\n**/\ntype CaptchaTokenRequest struct {\n\tAction       string            `json:\"action\"`\n\tCaptchaToken string            `json:\"captcha_token\"`\n\tClientID     string            `json:\"client_id\"`\n\tDeviceID     string            `json:\"device_id\"`\n\tMeta         map[string]string `json:\"meta\"`\n\tRedirectUri  string            `json:\"redirect_uri\"`\n}\n\ntype CaptchaTokenResponse struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tUrl          string `json:\"url\"`\n}\n\n/*\n* 登录\n**/\ntype TokenResp struct {\n\tTokenType    string `json:\"token_type\"`\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\n\tSub    string `json:\"sub\"`\n\tUserID string `json:\"user_id\"`\n}\n\nfunc (t *TokenResp) Token() string {\n\treturn fmt.Sprint(t.TokenType, \" \", t.AccessToken)\n}\n\ntype SignInRequest struct {\n\tCaptchaToken string `json:\"captcha_token\"`\n\n\tClientID     string `json:\"client_id\"`\n\tClientSecret string `json:\"client_secret\"`\n\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\n/*\n* 文件\n**/\ntype FileList struct {\n\tKind            string  `json:\"kind\"`\n\tNextPageToken   string  `json:\"next_page_token\"`\n\tFiles           []Files `json:\"files\"`\n\tVersion         string  `json:\"version\"`\n\tVersionOutdated bool    `json:\"version_outdated\"`\n}\n\ntype Link struct {\n\tURL    string    `json:\"url\"`\n\tToken  string    `json:\"token\"`\n\tExpire time.Time `json:\"expire\"`\n\tType   string    `json:\"type\"`\n}\n\nvar _ model.Obj = (*Files)(nil)\n\ntype Files struct {\n\tKind     string `json:\"kind\"`\n\tID       string `json:\"id\"`\n\tParentID string `json:\"parent_id\"`\n\tName     string `json:\"name\"`\n\t//UserID         string    `json:\"user_id\"`\n\tSize string `json:\"size\"`\n\t//Revision       string    `json:\"revision\"`\n\t//FileExtension  string    `json:\"file_extension\"`\n\t//MimeType       string    `json:\"mime_type\"`\n\t//Starred        bool      `json:\"starred\"`\n\tWebContentLink string    `json:\"web_content_link\"`\n\tCreatedTime    time.Time `json:\"created_time\"`\n\tModifiedTime   time.Time `json:\"modified_time\"`\n\tIconLink       string    `json:\"icon_link\"`\n\tThumbnailLink  string    `json:\"thumbnail_link\"`\n\t// Md5Checksum    string    `json:\"md5_checksum\"`\n\tHash string `json:\"hash\"`\n\t// Links map[string]Link `json:\"links\"`\n\t// Phase string          `json:\"phase\"`\n\t// Audit struct {\n\t// \tStatus  string `json:\"status\"`\n\t// \tMessage string `json:\"message\"`\n\t// \tTitle   string `json:\"title\"`\n\t// } `json:\"audit\"`\n\tMedias []struct {\n\t\t//Category       string `json:\"category\"`\n\t\t//IconLink       string `json:\"icon_link\"`\n\t\t//IsDefault      bool   `json:\"is_default\"`\n\t\t//IsOrigin       bool   `json:\"is_origin\"`\n\t\t//IsVisible      bool   `json:\"is_visible\"`\n\t\tLink Link `json:\"link\"`\n\t\t//MediaID        string `json:\"media_id\"`\n\t\t//MediaName      string `json:\"media_name\"`\n\t\t//NeedMoreQuota  bool   `json:\"need_more_quota\"`\n\t\t//Priority       int    `json:\"priority\"`\n\t\t//RedirectLink   string `json:\"redirect_link\"`\n\t\t//ResolutionName string `json:\"resolution_name\"`\n\t\t// Video          struct {\n\t\t// \tAudioCodec string `json:\"audio_codec\"`\n\t\t// \tBitRate    int    `json:\"bit_rate\"`\n\t\t// \tDuration   int    `json:\"duration\"`\n\t\t// \tFrameRate  int    `json:\"frame_rate\"`\n\t\t// \tHeight     int    `json:\"height\"`\n\t\t// \tVideoCodec string `json:\"video_codec\"`\n\t\t// \tVideoType  string `json:\"video_type\"`\n\t\t// \tWidth      int    `json:\"width\"`\n\t\t// } `json:\"video\"`\n\t\t// VipTypes []string `json:\"vip_types\"`\n\t} `json:\"medias\"`\n\tTrashed     bool   `json:\"trashed\"`\n\tDeleteTime  string `json:\"delete_time\"`\n\tOriginalURL string `json:\"original_url\"`\n\t//Params            struct{} `json:\"params\"`\n\t//OriginalFileIndex int    `json:\"original_file_index\"`\n\t//Space             string `json:\"space\"`\n\t//Apps              []interface{} `json:\"apps\"`\n\t//Writable   bool   `json:\"writable\"`\n\t//FolderType string `json:\"folder_type\"`\n\t//Collection interface{} `json:\"collection\"`\n}\n\nfunc (c *Files) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(hash_extend.GCID, c.Hash)\n}\n\nfunc (c *Files) GetSize() int64        { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }\nfunc (c *Files) GetName() string       { return c.Name }\nfunc (c *Files) CreateTime() time.Time { return c.CreatedTime }\nfunc (c *Files) ModTime() time.Time    { return c.ModifiedTime }\nfunc (c *Files) IsDir() bool           { return c.Kind == FOLDER }\nfunc (c *Files) GetID() string         { return c.ID }\nfunc (c *Files) GetPath() string       { return \"\" }\nfunc (c *Files) Thumb() string         { return c.ThumbnailLink }\n\n/*\n* 上传\n**/\ntype UploadTaskResponse struct {\n\tUploadType string `json:\"upload_type\"`\n\n\t/*//UPLOAD_TYPE_FORM\n\tForm struct {\n\t\t//Headers struct{} `json:\"headers\"`\n\t\tKind       string `json:\"kind\"`\n\t\tMethod     string `json:\"method\"`\n\t\tMultiParts struct {\n\t\t\tOSSAccessKeyID string `json:\"OSSAccessKeyId\"`\n\t\t\tSignature      string `json:\"Signature\"`\n\t\t\tCallback       string `json:\"callback\"`\n\t\t\tKey            string `json:\"key\"`\n\t\t\tPolicy         string `json:\"policy\"`\n\t\t\tXUserData      string `json:\"x:user_data\"`\n\t\t} `json:\"multi_parts\"`\n\t\tURL string `json:\"url\"`\n\t} `json:\"form\"`*/\n\n\t//UPLOAD_TYPE_RESUMABLE\n\tResumable struct {\n\t\tKind   string `json:\"kind\"`\n\t\tParams struct {\n\t\t\tAccessKeyID     string    `json:\"access_key_id\"`\n\t\t\tAccessKeySecret string    `json:\"access_key_secret\"`\n\t\t\tBucket          string    `json:\"bucket\"`\n\t\t\tEndpoint        string    `json:\"endpoint\"`\n\t\t\tExpiration      time.Time `json:\"expiration\"`\n\t\t\tKey             string    `json:\"key\"`\n\t\t\tSecurityToken   string    `json:\"security_token\"`\n\t\t} `json:\"params\"`\n\t\tProvider string `json:\"provider\"`\n\t} `json:\"resumable\"`\n\n\tFile Files `json:\"file\"`\n}\n\n// 添加离线下载响应\ntype OfflineDownloadResp struct {\n\tFile       *string     `json:\"file\"`\n\tTask       OfflineTask `json:\"task\"`\n\tUploadType string      `json:\"upload_type\"`\n\tURL        struct {\n\t\tKind string `json:\"kind\"`\n\t} `json:\"url\"`\n}\n\n// 离线下载列表\ntype OfflineListResp struct {\n\tExpiresIn     int64         `json:\"expires_in\"`\n\tNextPageToken string        `json:\"next_page_token\"`\n\tTasks         []OfflineTask `json:\"tasks\"`\n}\n\n// offlineTask\ntype OfflineTask struct {\n\tCallback          string            `json:\"callback\"`\n\tCreatedTime       string            `json:\"created_time\"`\n\tFileID            string            `json:\"file_id\"`\n\tFileName          string            `json:\"file_name\"`\n\tFileSize          string            `json:\"file_size\"`\n\tIconLink          string            `json:\"icon_link\"`\n\tID                string            `json:\"id\"`\n\tKind              string            `json:\"kind\"`\n\tMessage           string            `json:\"message\"`\n\tName              string            `json:\"name\"`\n\tParams            Params            `json:\"params\"`\n\tPhase             string            `json:\"phase\"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING\n\tProgress          int64             `json:\"progress\"`\n\tReferenceResource ReferenceResource `json:\"reference_resource\"`\n\tSpace             string            `json:\"space\"`\n\tStatusSize        int64             `json:\"status_size\"`\n\tStatuses          []string          `json:\"statuses\"`\n\tThirdTaskID       string            `json:\"third_task_id\"`\n\tType              string            `json:\"type\"`\n\tUpdatedTime       string            `json:\"updated_time\"`\n\tUserID            string            `json:\"user_id\"`\n}\n\ntype Params struct {\n\tAge         string  `json:\"age\"`\n\tMIMEType    *string `json:\"mime_type,omitempty\"`\n\tPredictType string  `json:\"predict_type\"`\n\tURL         string  `json:\"url\"`\n}\n\ntype ReferenceResource struct {\n\tType          string                 `json:\"@type\"`\n\tAudit         interface{}            `json:\"audit\"`\n\tHash          string                 `json:\"hash\"`\n\tIconLink      string                 `json:\"icon_link\"`\n\tID            string                 `json:\"id\"`\n\tKind          string                 `json:\"kind\"`\n\tMedias        []Media                `json:\"medias\"`\n\tMIMEType      string                 `json:\"mime_type\"`\n\tName          string                 `json:\"name\"`\n\tParams        map[string]interface{} `json:\"params\"`\n\tParentID      string                 `json:\"parent_id\"`\n\tPhase         string                 `json:\"phase\"`\n\tSize          string                 `json:\"size\"`\n\tSpace         string                 `json:\"space\"`\n\tStarred       bool                   `json:\"starred\"`\n\tTags          []string               `json:\"tags\"`\n\tThumbnailLink string                 `json:\"thumbnail_link\"`\n}\n\ntype Media struct {\n\tMediaId   string `json:\"media_id\"`\n\tMediaName string `json:\"media_name\"`\n\tVideo     struct {\n\t\tHeight     int    `json:\"height\"`\n\t\tWidth      int    `json:\"width\"`\n\t\tDuration   int    `json:\"duration\"`\n\t\tBitRate    int    `json:\"bit_rate\"`\n\t\tFrameRate  int    `json:\"frame_rate\"`\n\t\tVideoCodec string `json:\"video_codec\"`\n\t\tAudioCodec string `json:\"audio_codec\"`\n\t\tVideoType  string `json:\"video_type\"`\n\t} `json:\"video\"`\n\tLink struct {\n\t\tUrl    string    `json:\"url\"`\n\t\tToken  string    `json:\"token\"`\n\t\tExpire time.Time `json:\"expire\"`\n\t} `json:\"link\"`\n\tNeedMoreQuota  bool          `json:\"need_more_quota\"`\n\tVipTypes       []interface{} `json:\"vip_types\"`\n\tRedirectLink   string        `json:\"redirect_link\"`\n\tIconLink       string        `json:\"icon_link\"`\n\tIsDefault      bool          `json:\"is_default\"`\n\tPriority       int           `json:\"priority\"`\n\tIsOrigin       bool          `json:\"is_origin\"`\n\tResolutionName string        `json:\"resolution_name\"`\n\tIsVisible      bool          `json:\"is_visible\"`\n\tCategory       string        `json:\"category\"`\n}\n\ntype AboutResponse struct {\n\tQuota struct {\n\t\tLimit string `json:\"limit\"`\n\t\tUsage string `json:\"usage\"`\n\t} `json:\"quota\"`\n}\n"
  },
  {
    "path": "drivers/thunderx/util.go",
    "content": "package thunderx\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tAPI_URL        = \"https://api-pan.xunleix.com/drive/v1\"\n\tFILE_API_URL   = API_URL + \"/files\"\n\tTASKS_API_URL  = API_URL + \"/tasks\"\n\tXLUSER_API_URL = \"https://xluser-ssl.xunleix.com/v1\"\n)\n\nvar Algorithms = []string{\n\t\"kVy0WbPhiE4v6oxXZ88DvoA3Q\",\n\t\"lON/AUoZKj8/nBtcE85mVbkOaVdVa\",\n\t\"rLGffQrfBKH0BgwQ33yZofvO3Or\",\n\t\"FO6HWqw\",\n\t\"GbgvyA2\",\n\t\"L1NU9QvIQIH7DTRt\",\n\t\"y7llk4Y8WfYflt6\",\n\t\"iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe\",\n\t\"8C28RTXmVcco0\",\n\t\"X5Xh\",\n\t\"7xe25YUgfGgD0xW3ezFS\",\n\t\"\",\n\t\"CKCR\",\n\t\"8EmDjBo6h3eLaK7U6vU2Qys0NsMx\",\n\t\"t2TeZBXKqbdP09Arh9C3\",\n}\n\nconst (\n\tClientID          = \"ZQL_zwA4qhHcoe_2\"\n\tClientSecret      = \"Og9Vr1L8Ee6bh0olFxFDRg\"\n\tClientVersion     = \"1.06.0.2132\"\n\tPackageName       = \"com.thunder.downloader\"\n\tDownloadUserAgent = \"Dalvik/2.1.0 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)\"\n\tSdkVersion        = \"2.0.3.203100 \"\n)\n\nconst (\n\tFOLDER    = \"drive#folder\"\n\tFILE      = \"drive#file\"\n\tRESUMABLE = \"drive#resumable\"\n)\n\nconst (\n\tUPLOAD_TYPE_UNKNOWN = \"UPLOAD_TYPE_UNKNOWN\"\n\t//UPLOAD_TYPE_FORM      = \"UPLOAD_TYPE_FORM\"\n\tUPLOAD_TYPE_RESUMABLE = \"UPLOAD_TYPE_RESUMABLE\"\n\tUPLOAD_TYPE_URL       = \"UPLOAD_TYPE_URL\"\n)\n\nfunc GetAction(method string, url string) string {\n\turlpath := regexp.MustCompile(`://[^/]+((/[^/\\s?#]+)*)`).FindStringSubmatch(url)[1]\n\treturn method + \":\" + urlpath\n}\n\ntype Common struct {\n\tclient *resty.Client\n\n\tcaptchaToken string\n\tuserID       string\n\t// 签名相关,二选一\n\tAlgorithms             []string\n\tTimestamp, CaptchaSign string\n\n\t// 必要值,签名相关\n\tDeviceID          string\n\tClientID          string\n\tClientSecret      string\n\tClientVersion     string\n\tPackageName       string\n\tUserAgent         string\n\tDownloadUserAgent string\n\tUseVideoUrl       bool\n\n\t// 验证码token刷新成功回调\n\trefreshCTokenCk func(token string)\n}\n\nfunc (c *Common) SetDeviceID(deviceID string) {\n\tc.DeviceID = deviceID\n}\n\nfunc (c *Common) SetUserID(userID string) {\n\tc.userID = userID\n}\n\nfunc (c *Common) SetUserAgent(userAgent string) {\n\tc.UserAgent = userAgent\n}\n\nfunc (c *Common) SetCaptchaToken(captchaToken string) {\n\tc.captchaToken = captchaToken\n}\nfunc (c *Common) GetCaptchaToken() string {\n\treturn c.captchaToken\n}\n\n// 刷新验证码token(登录后)\nfunc (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {\n\tmetas := map[string]string{\n\t\t\"client_version\": c.ClientVersion,\n\t\t\"package_name\":   c.PackageName,\n\t\t\"user_id\":        userID,\n\t}\n\tmetas[\"timestamp\"], metas[\"captcha_sign\"] = c.GetCaptchaSign()\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// 刷新验证码token(登录时)\nfunc (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {\n\tmetas := make(map[string]string)\n\tif ok, _ := regexp.MatchString(`\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*`, username); ok {\n\t\tmetas[\"email\"] = username\n\t} else if len(username) >= 11 && len(username) <= 18 {\n\t\tmetas[\"phone_number\"] = username\n\t} else {\n\t\tmetas[\"username\"] = username\n\t}\n\treturn c.refreshCaptchaToken(action, metas)\n}\n\n// 获取验证码签名\nfunc (c *Common) GetCaptchaSign() (timestamp, sign string) {\n\tif len(c.Algorithms) == 0 {\n\t\treturn c.Timestamp, c.CaptchaSign\n\t}\n\ttimestamp = fmt.Sprint(time.Now().UnixMilli())\n\tstr := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)\n\tfor _, algorithm := range c.Algorithms {\n\t\tstr = utils.GetMD5EncodeStr(str + algorithm)\n\t}\n\tsign = \"1.\" + str\n\treturn\n}\n\n// 刷新验证码token\nfunc (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {\n\tparam := CaptchaTokenRequest{\n\t\tAction:       action,\n\t\tCaptchaToken: c.captchaToken,\n\t\tClientID:     c.ClientID,\n\t\tDeviceID:     c.DeviceID,\n\t\tMeta:         metas,\n\t\tRedirectUri:  \"xlaccsdk01://xbase.cloud/callback?state=harbor\",\n\t}\n\tvar e ErrResp\n\tvar resp CaptchaTokenResponse\n\t_, err := c.Request(XLUSER_API_URL+\"/shield/captcha/init\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetError(&e).SetBody(param)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif e.IsError() {\n\t\treturn &e\n\t}\n\n\tif resp.Url != \"\" {\n\t\treturn fmt.Errorf(`need verify: <a target=\"_blank\" href=\"%s\">Click Here</a>`, resp.Url)\n\t}\n\n\tif resp.CaptchaToken == \"\" {\n\t\treturn fmt.Errorf(\"empty captchaToken\")\n\t}\n\n\tif c.refreshCTokenCk != nil {\n\t\tc.refreshCTokenCk(resp.CaptchaToken)\n\t}\n\tc.SetCaptchaToken(resp.CaptchaToken)\n\treturn nil\n}\n\n// Request 只有基础信息的请求\nfunc (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := c.client.R().SetHeaders(map[string]string{\n\t\t\"user-agent\":       c.UserAgent,\n\t\t\"accept\":           \"application/json;charset=UTF-8\",\n\t\t\"x-device-id\":      c.DeviceID,\n\t\t\"x-client-id\":      c.ClientID,\n\t\t\"x-client-version\": c.ClientVersion,\n\t})\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar erron ErrResp\n\tutils.Json.Unmarshal(res.Body(), &erron)\n\tif erron.IsError() {\n\t\treturn nil, &erron\n\t}\n\n\treturn res.Body(), nil\n}\n\n// 计算文件Gcid\nfunc getGcid(r io.Reader, size int64) (string, error) {\n\tcalcBlockSize := func(j int64) int64 {\n\t\tvar psize int64 = 0x40000\n\t\tfor float64(j)/float64(psize) > 0x200 && psize < 0x200000 {\n\t\t\tpsize = psize << 1\n\t\t}\n\t\treturn psize\n\t}\n\n\thash1 := sha1.New()\n\thash2 := sha1.New()\n\treadSize := calcBlockSize(size)\n\tfor {\n\t\thash2.Reset()\n\t\tif n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {\n\t\t\tif err != io.EOF {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\thash1.Write(hash2.Sum(nil))\n\t}\n\treturn hex.EncodeToString(hash1.Sum(nil)), nil\n}\n\nfunc generateDeviceSign(deviceID, packageName string) string {\n\n\tsignatureBase := fmt.Sprintf(\"%s%s%s%s\", deviceID, packageName, \"1\", \"appkey\")\n\n\tsha1Hash := sha1.New()\n\tsha1Hash.Write([]byte(signatureBase))\n\tsha1Result := sha1Hash.Sum(nil)\n\n\tsha1String := hex.EncodeToString(sha1Result)\n\n\tmd5Hash := md5.New()\n\tmd5Hash.Write([]byte(sha1String))\n\tmd5Result := md5Hash.Sum(nil)\n\n\tmd5String := hex.EncodeToString(md5Result)\n\n\tdeviceSign := fmt.Sprintf(\"div101.%s%s\", deviceID, md5String)\n\n\treturn deviceSign\n}\n\nfunc BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {\n\tdeviceSign := generateDeviceSign(deviceID, packageName)\n\tvar sb strings.Builder\n\n\tsb.WriteString(fmt.Sprintf(\"ANDROID-%s/%s \", appName, clientVersion))\n\tsb.WriteString(\"protocolVersion/200 \")\n\tsb.WriteString(\"accesstype/ \")\n\tsb.WriteString(fmt.Sprintf(\"clientid/%s \", clientID))\n\tsb.WriteString(fmt.Sprintf(\"clientversion/%s \", clientVersion))\n\tsb.WriteString(\"action_type/ \")\n\tsb.WriteString(\"networktype/WIFI \")\n\tsb.WriteString(\"sessionid/ \")\n\tsb.WriteString(fmt.Sprintf(\"deviceid/%s \", deviceID))\n\tsb.WriteString(\"providername/NONE \")\n\tsb.WriteString(fmt.Sprintf(\"devicesign/%s \", deviceSign))\n\tsb.WriteString(\"refresh_token/ \")\n\tsb.WriteString(fmt.Sprintf(\"sdkversion/%s \", sdkVersion))\n\tsb.WriteString(fmt.Sprintf(\"datetime/%d \", time.Now().UnixMilli()))\n\tsb.WriteString(fmt.Sprintf(\"usrno/%s \", userID))\n\tsb.WriteString(fmt.Sprintf(\"appname/%s \", appName))\n\tsb.WriteString(fmt.Sprintf(\"session_origin/ \"))\n\tsb.WriteString(fmt.Sprintf(\"grant_type/ \"))\n\tsb.WriteString(fmt.Sprintf(\"appid/ \"))\n\tsb.WriteString(fmt.Sprintf(\"clientip/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicename/Xiaomi_M2004j7ac \"))\n\tsb.WriteString(fmt.Sprintf(\"osversion/13 \"))\n\tsb.WriteString(fmt.Sprintf(\"platformversion/10 \"))\n\tsb.WriteString(fmt.Sprintf(\"accessmode/ \"))\n\tsb.WriteString(fmt.Sprintf(\"devicemodel/M2004J7AC \"))\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "drivers/url_tree/driver.go",
    "content": "package url_tree\n\nimport (\n\t\"context\"\n\t\"errors\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Urls struct {\n\tmodel.Storage\n\tAddition\n\troot  *Node\n\tmutex sync.RWMutex\n}\n\nfunc (d *Urls) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Urls) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Urls) Init(ctx context.Context) error {\n\tnode, err := BuildTree(d.UrlStructure, d.HeadSize)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnode.calSize()\n\td.root = node\n\treturn nil\n}\n\nfunc (d *Urls) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (Addition) GetRootPath() string {\n\treturn \"/\"\n}\n\nfunc (d *Urls) Get(ctx context.Context, path string) (model.Obj, error) {\n\td.mutex.RLock()\n\tdefer d.mutex.RUnlock()\n\tnode := GetNodeFromRootByPath(d.root, path)\n\treturn nodeToObj(node, path)\n}\n\nfunc (d *Urls) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\td.mutex.RLock()\n\tdefer d.mutex.RUnlock()\n\tnode := GetNodeFromRootByPath(d.root, dir.GetPath())\n\tlog.Debugf(\"path: %s, node: %+v\", dir.GetPath(), node)\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\treturn utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {\n\t\treturn nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))\n\t})\n}\n\nfunc (d *Urls) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\td.mutex.RLock()\n\tdefer d.mutex.RUnlock()\n\tnode := GetNodeFromRootByPath(d.root, file.GetPath())\n\tlog.Debugf(\"path: %s, node: %+v\", file.GetPath(), node)\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\treturn &model.Link{\n\t\t\tURL: node.Url,\n\t\t}, nil\n\t}\n\treturn nil, errs.NotFile\n}\n\nfunc (d *Urls) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif !d.Writable {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tnode := GetNodeFromRootByPath(d.root, parentDir.GetPath())\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\tdir := &Node{\n\t\tName:  dirName,\n\t\tLevel: node.Level + 1,\n\t}\n\tnode.Children = append(node.Children, dir)\n\td.updateStorage()\n\treturn nodeToObj(dir, stdpath.Join(parentDir.GetPath(), dirName))\n}\n\nfunc (d *Urls) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif !d.Writable {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\tif strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {\n\t\treturn nil, errors.New(\"cannot move parent dir to child\")\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tdstNode := GetNodeFromRootByPath(d.root, dstDir.GetPath())\n\tif dstNode == nil || dstNode.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\tsrcDir, srcName := stdpath.Split(srcObj.GetPath())\n\tsrcParentNode := GetNodeFromRootByPath(d.root, srcDir)\n\tif srcParentNode == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tnewChildren := make([]*Node, 0, len(srcParentNode.Children))\n\tvar srcNode *Node\n\tfor _, child := range srcParentNode.Children {\n\t\tif child.Name == srcName {\n\t\t\tsrcNode = child\n\t\t} else {\n\t\t\tnewChildren = append(newChildren, child)\n\t\t}\n\t}\n\tif srcNode == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tsrcParentNode.Children = newChildren\n\tsrcNode.setLevel(dstNode.Level + 1)\n\tdstNode.Children = append(dstNode.Children, srcNode)\n\td.root.calSize()\n\td.updateStorage()\n\treturn nodeToObj(srcNode, stdpath.Join(dstDir.GetPath(), srcName))\n}\n\nfunc (d *Urls) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif !d.Writable {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tsrcNode := GetNodeFromRootByPath(d.root, srcObj.GetPath())\n\tif srcNode == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tsrcNode.Name = newName\n\td.updateStorage()\n\treturn nodeToObj(srcNode, stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))\n}\n\nfunc (d *Urls) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif !d.Writable {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\tif strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {\n\t\treturn nil, errors.New(\"cannot copy parent dir to child\")\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tdstNode := GetNodeFromRootByPath(d.root, dstDir.GetPath())\n\tif dstNode == nil || dstNode.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\tsrcNode := GetNodeFromRootByPath(d.root, srcObj.GetPath())\n\tif srcNode == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tnewNode := srcNode.deepCopy(dstNode.Level + 1)\n\tdstNode.Children = append(dstNode.Children, newNode)\n\td.root.calSize()\n\td.updateStorage()\n\treturn nodeToObj(newNode, stdpath.Join(dstDir.GetPath(), stdpath.Base(srcObj.GetPath())))\n}\n\nfunc (d *Urls) Remove(ctx context.Context, obj model.Obj) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tobjDir, objName := stdpath.Split(obj.GetPath())\n\tnodeParent := GetNodeFromRootByPath(d.root, objDir)\n\tif nodeParent == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\tnewChildren := make([]*Node, 0, len(nodeParent.Children))\n\tvar deletedObj *Node\n\tfor _, child := range nodeParent.Children {\n\t\tif child.Name != objName {\n\t\t\tnewChildren = append(newChildren, child)\n\t\t} else {\n\t\t\tdeletedObj = child\n\t\t}\n\t}\n\tif deletedObj == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\tnodeParent.Children = newChildren\n\tif deletedObj.Size > 0 {\n\t\td.root.calSize()\n\t}\n\td.updateStorage()\n\treturn nil\n}\n\nfunc (d *Urls) PutURL(ctx context.Context, dstDir model.Obj, name, url string) (model.Obj, error) {\n\tif !d.Writable {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tdirNode := GetNodeFromRootByPath(d.root, dstDir.GetPath())\n\tif dirNode == nil || dirNode.isFile() {\n\t\treturn nil, errs.NotFolder\n\t}\n\tnewNode := &Node{\n\t\tName:  name,\n\t\tLevel: dirNode.Level + 1,\n\t\tUrl:   url,\n\t}\n\tdirNode.Children = append(dirNode.Children, newNode)\n\tif d.HeadSize {\n\t\tsize, err := getSizeFromUrl(url)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get size from url error: %s\", err)\n\t\t} else {\n\t\t\tnewNode.Size = size\n\t\t\td.root.calSize()\n\t\t}\n\t}\n\td.updateStorage()\n\treturn nodeToObj(newNode, stdpath.Join(dstDir.GetPath(), name))\n}\n\nfunc (d *Urls) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\tnode := GetNodeFromRootByPath(d.root, dstDir.GetPath()) // parent\n\tif node == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\tif node.isFile() {\n\t\treturn errs.NotFolder\n\t}\n\tfile, err := parseFileLine(stream.GetName(), d.HeadSize)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnode.Children = append(node.Children, file)\n\td.updateStorage()\n\treturn nil\n}\n\nfunc (d *Urls) updateStorage() {\n\td.UrlStructure = StringifyTree(d.root)\n\top.MustSaveDriverStorage(d)\n}\n\n//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Urls)(nil)\n"
  },
  {
    "path": "drivers/url_tree/meta.go",
    "content": "package url_tree\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tUrlStructure string `json:\"url_structure\" type:\"text\" required:\"true\" default:\"https://raw.githubusercontent.com/OpenListTeam/OpenList/main/README.md\\nhttps://raw.githubusercontent.com/OpenListTeam/OpenList/main/README_cn.md\\nfolder:\\n  CONTRIBUTING.md:1635:https://raw.githubusercontent.com/OpenListTeam/OpenList/main/CONTRIBUTING.md\\n  CODE_OF_CONDUCT.md:2093:https://raw.githubusercontent.com/OpenListTeam/OpenList/main/CODE_OF_CONDUCT.md\" help:\"structure:FolderName:\\n  [FileName:][FileSize:][Modified:]Url\"`\n\tHeadSize     bool   `json:\"head_size\" type:\"bool\" default:\"false\" help:\"Use head method to get file size, but it may be failed.\"`\n\tWritable     bool   `json:\"writable\" type:\"bool\" default:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:        \"UrlTree\",\n\tLocalSort:   true,\n\tNoCache:     true,\n\tCheckStatus: true,\n\tOnlyIndices: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Urls{}\n\t})\n}\n"
  },
  {
    "path": "drivers/url_tree/types.go",
    "content": "package url_tree\n\nimport \"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n// Node is a node in the folder tree\ntype Node struct {\n\tUrl      string\n\tName     string\n\tLevel    int\n\tModified int64\n\tSize     int64\n\tChildren []*Node\n}\n\nfunc (node *Node) getByPath(paths []string) *Node {\n\tif len(paths) == 0 || node == nil {\n\t\treturn nil\n\t}\n\tif node.Name != paths[0] {\n\t\treturn nil\n\t}\n\tif len(paths) == 1 {\n\t\treturn node\n\t}\n\tfor _, child := range node.Children {\n\t\ttmp := child.getByPath(paths[1:])\n\t\tif tmp != nil {\n\t\t\treturn tmp\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (node *Node) isFile() bool {\n\treturn node.Url != \"\"\n}\n\nfunc (node *Node) calSize() int64 {\n\tif node.isFile() {\n\t\treturn node.Size\n\t}\n\tvar size int64 = 0\n\tfor _, child := range node.Children {\n\t\tsize += child.calSize()\n\t}\n\tnode.Size = size\n\treturn size\n}\n\nfunc (node *Node) setLevel(level int) {\n\tnode.Level = level\n\tfor _, child := range node.Children {\n\t\tchild.setLevel(level + 1)\n\t}\n}\n\nfunc (node *Node) deepCopy(level int) *Node {\n\tret := *node\n\tret.Level = level\n\tret.Children, _ = utils.SliceConvert(ret.Children, func(child *Node) (*Node, error) {\n\t\treturn child.deepCopy(level + 1), nil\n\t})\n\treturn &ret\n}\n"
  },
  {
    "path": "drivers/url_tree/urls_test.go",
    "content": "package url_tree_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/url_tree\"\n)\n\nfunc testTree() (*url_tree.Node, error) {\n\ttext := `folder1:\n  name1:https://url1\n  http://url2\n  folder2:\n    http://url3\n    http://url4\n  http://url5\nfolder3:\n  http://url6\n  http://url7\nhttp://url8`\n\treturn url_tree.BuildTree(text, false)\n}\n\nfunc TestBuildTree(t *testing.T) {\n\tnode, err := testTree()\n\tif err != nil {\n\t\tt.Errorf(\"failed to build tree: %+v\", err)\n\t} else {\n\t\tt.Logf(\"tree: %+v\", node)\n\t}\n}\n\nfunc TestGetNode(t *testing.T) {\n\troot, err := testTree()\n\tif err != nil {\n\t\tt.Errorf(\"failed to build tree: %+v\", err)\n\t\treturn\n\t}\n\tnode := url_tree.GetNodeFromRootByPath(root, \"/\")\n\tif node != root {\n\t\tt.Errorf(\"got wrong node: %+v\", node)\n\t}\n\turl3 := url_tree.GetNodeFromRootByPath(root, \"/folder1/folder2/url3\")\n\tif url3 != root.Children[0].Children[2].Children[0] {\n\t\tt.Errorf(\"got wrong node: %+v\", url3)\n\t}\n}\n"
  },
  {
    "path": "drivers/url_tree/util.go",
    "content": "package url_tree\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// build tree from text, text structure definition:\n/**\n * FolderName:\n *   [FileName:][FileSize:][Modified:]Url\n */\n/**\n * For example:\n * folder1:\n *   name1:url1\n *   url2\n *   folder2:\n *     url3\n *     url4\n *   url5\n * folder3:\n *   url6\n *   url7\n * url8\n */\n// if there are no name, use the last segment of url as name\nfunc BuildTree(text string, headSize bool) (*Node, error) {\n\tlines := strings.Split(text, \"\\n\")\n\tvar root = &Node{Level: -1, Name: \"root\"}\n\tstack := []*Node{root}\n\tfor _, line := range lines {\n\t\t// calculate indent\n\t\tindent := 0\n\t\tfor i := 0; i < len(line); i++ {\n\t\t\tif line[i] != ' ' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tindent++\n\t\t}\n\t\t// if indent is not a multiple of 2, it is an error\n\t\tif indent%2 != 0 {\n\t\t\treturn nil, fmt.Errorf(\"the line '%s' is not a multiple of 2\", line)\n\t\t}\n\t\t// calculate level\n\t\tlevel := indent / 2\n\t\tline = strings.TrimSpace(line[indent:])\n\t\t// if the line is empty, skip\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// if level isn't greater than the level of the top of the stack\n\t\t// it is not the child of the top of the stack\n\t\tfor level <= stack[len(stack)-1].Level {\n\t\t\t// pop the top of the stack\n\t\t\tstack = stack[:len(stack)-1]\n\t\t}\n\t\t// if the line is a folder\n\t\tif isFolder(line) {\n\t\t\t// create a new node\n\t\t\tnode := &Node{\n\t\t\t\tLevel: level,\n\t\t\t\tName:  strings.TrimSuffix(line, \":\"),\n\t\t\t}\n\t\t\t// add the node to the top of the stack\n\t\t\tstack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)\n\t\t\t// push the node to the stack\n\t\t\tstack = append(stack, node)\n\t\t} else {\n\t\t\t// if the line is a file\n\t\t\t// create a new node\n\t\t\tnode, err := parseFileLine(line, headSize)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tnode.Level = level\n\t\t\t// add the node to the top of the stack\n\t\t\tstack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)\n\t\t}\n\t}\n\treturn root, nil\n}\n\nfunc isFolder(line string) bool {\n\treturn strings.HasSuffix(line, \":\")\n}\n\n// line definition:\n// [FileName:][FileSize:][Modified:]Url\nfunc parseFileLine(line string, headSize bool) (*Node, error) {\n\t// if there is no url, it is an error\n\tif !strings.Contains(line, \"http://\") && !strings.Contains(line, \"https://\") {\n\t\treturn nil, fmt.Errorf(\"invalid line: %s, because url is required for file\", line)\n\t}\n\tindex := strings.Index(line, \"http://\")\n\tif index == -1 {\n\t\tindex = strings.Index(line, \"https://\")\n\t}\n\turl := line[index:]\n\tinfo := line[:index]\n\tnode := &Node{\n\t\tUrl: url,\n\t}\n\thaveSize := false\n\tif index > 0 {\n\t\tif !strings.HasSuffix(info, \":\") {\n\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file info must end with ':'\", line)\n\t\t}\n\t\tinfo = info[:len(info)-1]\n\t\tif info == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file name can't be empty\", line)\n\t\t}\n\t\tinfoParts := strings.Split(info, \":\")\n\t\tnode.Name = infoParts[0]\n\t\tif len(infoParts) > 1 {\n\t\t\tsize, err := strconv.ParseInt(infoParts[1], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file size must be an integer\", line)\n\t\t\t}\n\t\t\tnode.Size = size\n\t\t\thaveSize = true\n\t\t\tif len(infoParts) > 2 {\n\t\t\t\tmodified, err := strconv.ParseInt(infoParts[2], 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"invalid line: %s, because file modified must be an unix timestamp\", line)\n\t\t\t\t}\n\t\t\t\tnode.Modified = modified\n\t\t\t}\n\t\t}\n\t} else {\n\t\tnode.Name = stdpath.Base(url)\n\t}\n\tif !haveSize && headSize {\n\t\tsize, err := getSizeFromUrl(url)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get size from url error: %s\", err)\n\t\t} else {\n\t\t\tnode.Size = size\n\t\t}\n\t}\n\treturn node, nil\n}\n\nfunc splitPath(path string) []string {\n\tif path == \"/\" {\n\t\treturn []string{\"root\"}\n\t}\n\tif strings.HasSuffix(path, \"/\") {\n\t\tpath = path[:len(path)-1]\n\t}\n\tparts := strings.Split(path, \"/\")\n\tparts[0] = \"root\"\n\treturn parts\n}\n\nfunc GetNodeFromRootByPath(root *Node, path string) *Node {\n\treturn root.getByPath(splitPath(path))\n}\n\nfunc nodeToObj(node *Node, path string) (model.Obj, error) {\n\tif node == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\treturn &model.Object{\n\t\tName:     node.Name,\n\t\tSize:     node.Size,\n\t\tModified: time.Unix(node.Modified, 0),\n\t\tIsFolder: !node.isFile(),\n\t\tPath:     path,\n\t}, nil\n}\n\nfunc getSizeFromUrl(url string) (int64, error) {\n\tres, err := base.RestyClient.R().SetDoNotParseResponse(true).Head(url)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer res.RawResponse.Body.Close()\n\tif res.StatusCode() >= 300 {\n\t\treturn 0, fmt.Errorf(\"get size from url %s failed, status code: %d\", url, res.StatusCode())\n\t}\n\tsize, err := strconv.ParseInt(res.Header().Get(\"Content-Length\"), 10, 64)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn size, nil\n}\n\nfunc StringifyTree(node *Node) string {\n\tsb := strings.Builder{}\n\tif node.Level == -1 {\n\t\tfor i, child := range node.Children {\n\t\t\tsb.WriteString(StringifyTree(child))\n\t\t\tif i < len(node.Children)-1 {\n\t\t\t\tsb.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t\treturn sb.String()\n\t}\n\tfor i := 0; i < node.Level; i++ {\n\t\tsb.WriteString(\"  \")\n\t}\n\tif node.Url == \"\" {\n\t\tsb.WriteString(node.Name)\n\t\tsb.WriteString(\":\")\n\t\tfor _, child := range node.Children {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t\tsb.WriteString(StringifyTree(child))\n\t\t}\n\t} else if node.Size == 0 && node.Modified == 0 {\n\t\tif stdpath.Base(node.Url) == node.Name {\n\t\t\tsb.WriteString(node.Url)\n\t\t} else {\n\t\t\tsb.WriteString(fmt.Sprintf(\"%s:%s\", node.Name, node.Url))\n\t\t}\n\t} else {\n\t\tsb.WriteString(node.Name)\n\t\tsb.WriteString(\":\")\n\t\tif node.Size != 0 || node.Modified != 0 {\n\t\t\tsb.WriteString(strconv.FormatInt(node.Size, 10))\n\t\t\tsb.WriteString(\":\")\n\t\t}\n\t\tif node.Modified != 0 {\n\t\t\tsb.WriteString(strconv.FormatInt(node.Modified, 10))\n\t\t\tsb.WriteString(\":\")\n\t\t}\n\t\tsb.WriteString(node.Url)\n\t}\n\treturn sb.String()\n}\n"
  },
  {
    "path": "drivers/uss/driver.go",
    "content": "package uss\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/upyun/go-sdk/v3/upyun\"\n)\n\ntype USS struct {\n\tmodel.Storage\n\tAddition\n\tclient *upyun.UpYun\n}\n\nfunc (d *USS) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *USS) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *USS) Init(ctx context.Context) error {\n\td.client = upyun.NewUpYun(&upyun.UpYunConfig{\n\t\tBucket:   d.Bucket,\n\t\tOperator: d.OperatorName,\n\t\tPassword: d.OperatorPassword,\n\t})\n\treturn nil\n}\n\nfunc (d *USS) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *USS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tprefix := getKey(dir.GetPath(), true)\n\tobjsChan := make(chan *upyun.FileInfo, 10)\n\tvar err error\n\tgo func() {\n\t\terr = d.client.List(&upyun.GetObjectsConfig{\n\t\t\tPath:           prefix,\n\t\t\tObjectsChan:    objsChan,\n\t\t\tMaxListObjects: 0,\n\t\t\tMaxListLevel:   1,\n\t\t})\n\t}()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]model.Obj, 0)\n\tfor obj := range objsChan {\n\t\tt := obj.Time\n\t\tf := model.Object{\n\t\t\tPath:     path.Join(dir.GetPath(), obj.Name),\n\t\t\tName:     obj.Name,\n\t\t\tSize:     obj.Size,\n\t\t\tModified: t,\n\t\t\tIsFolder: obj.IsDir,\n\t\t}\n\t\tres = append(res, &f)\n\t}\n\treturn res, err\n}\n\nfunc (d *USS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tkey := getKey(file.GetPath(), false)\n\thost := d.Endpoint\n\tif !strings.Contains(host, \"://\") { //判断是否包含协议头，否则https\n\t\thost = \"https://\" + host\n\t}\n\tu := fmt.Sprintf(\"%s/%s\", host, key)\n\tdownExp := time.Hour * time.Duration(d.SignURLExpire)\n\texpireAt := time.Now().Add(downExp).Unix()\n\tupd := url.QueryEscape(path.Base(file.GetPath()))\n\ttokenOrPassword := d.AntiTheftChainToken\n\tif tokenOrPassword == \"\" {\n\t\ttokenOrPassword = d.OperatorPassword\n\t}\n\tsignStr := strings.Join([]string{tokenOrPassword, fmt.Sprint(expireAt), fmt.Sprintf(\"/%s\", key)}, \"&\")\n\tupt := utils.GetMD5EncodeStr(signStr)[12:20] + fmt.Sprint(expireAt)\n\tlink := fmt.Sprintf(\"%s?_upd=%s&_upt=%s\", u, upd, upt)\n\treturn &model.Link{URL: link}, nil\n}\n\nfunc (d *USS) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.client.Mkdir(getKey(path.Join(parentDir.GetPath(), dirName), true))\n}\n\nfunc (d *USS) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.client.Move(&upyun.MoveObjectConfig{\n\t\tSrcPath:  getKey(srcObj.GetPath(), srcObj.IsDir()),\n\t\tDestPath: getKey(path.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.IsDir()),\n\t})\n}\n\nfunc (d *USS) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn d.client.Move(&upyun.MoveObjectConfig{\n\t\tSrcPath:  getKey(srcObj.GetPath(), srcObj.IsDir()),\n\t\tDestPath: getKey(path.Join(path.Dir(srcObj.GetPath()), newName), srcObj.IsDir()),\n\t})\n}\n\nfunc (d *USS) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.client.Copy(&upyun.CopyObjectConfig{\n\t\tSrcPath:  getKey(srcObj.GetPath(), srcObj.IsDir()),\n\t\tDestPath: getKey(path.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.IsDir()),\n\t})\n}\n\nfunc (d *USS) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.client.Delete(&upyun.DeleteObjectConfig{\n\t\tPath:  getKey(obj.GetPath(), obj.IsDir()),\n\t\tAsync: false,\n\t})\n}\n\nfunc (d *USS) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\treturn d.client.Put(&upyun.PutObjectConfig{\n\t\tPath: getKey(path.Join(dstDir.GetPath(), s.GetName()), false),\n\t\tReader: driver.NewLimitedUploadStream(ctx, &stream.ReaderUpdatingProgress{\n\t\t\tReader:         s,\n\t\t\tUpdateProgress: up,\n\t\t}),\n\t})\n}\n\nvar _ driver.Driver = (*USS)(nil)\n"
  },
  {
    "path": "drivers/uss/meta.go",
    "content": "package uss\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tBucket              string `json:\"bucket\" required:\"true\"`\n\tEndpoint            string `json:\"endpoint\" required:\"true\"`\n\tOperatorName        string `json:\"operator_name\" required:\"true\"`\n\tOperatorPassword    string `json:\"operator_password\" required:\"true\"`\n\tAntiTheftChainToken string `json:\"anti_theft_chain_token\" required:\"false\" default:\"\"`\n\t//CustomHost       string `json:\"custom_host\"`\t//Endpoint与CustomHost作用相同，去除\n\tSignURLExpire int `json:\"sign_url_expire\" type:\"number\" default:\"4\"`\n}\n\nvar config = driver.Config{\n\tName:        \"USS\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &USS{}\n\t})\n}\n"
  },
  {
    "path": "drivers/uss/types.go",
    "content": "package uss\n"
  },
  {
    "path": "drivers/uss/util.go",
    "content": "package uss\n\nimport \"strings\"\n\n// do others that not defined in Driver interface\n\nfunc getKey(path string, dir bool) string {\n\tpath = strings.TrimPrefix(path, \"/\")\n\tif dir {\n\t\tpath += \"/\"\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "drivers/virtual/driver.go",
    "content": "package virtual\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n)\n\ntype Virtual struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *Virtual) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Virtual) Init(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Virtual) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Virtual) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Virtual) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar res []model.Obj\n\tfor i := 0; i < d.NumFile; i++ {\n\t\tres = append(res, d.genObj(false))\n\t}\n\tfor i := 0; i < d.NumFolder; i++ {\n\t\tres = append(res, d.genObj(true))\n\t}\n\treturn res, nil\n}\n\ntype DummyMFile struct{}\n\nfunc (f DummyMFile) Read(p []byte) (n int, err error) {\n\treturn random.Rand.Read(p)\n}\n\nfunc (f DummyMFile) ReadAt(p []byte, off int64) (n int, err error) {\n\treturn random.Rand.Read(p)\n}\n\nfunc (DummyMFile) Seek(offset int64, whence int) (int64, error) {\n\treturn offset, nil\n}\n\nfunc (d *Virtual) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\treturn &model.Link{\n\t\tRangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), DummyMFile{}),\n\t}, nil\n}\n\nfunc (d *Virtual) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tdir := &model.Object{\n\t\tName:     dirName,\n\t\tSize:     0,\n\t\tIsFolder: true,\n\t\tModified: time.Now(),\n\t}\n\treturn dir, nil\n}\n\nfunc (d *Virtual) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn srcObj, nil\n}\n\nfunc (d *Virtual) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tobj := &model.Object{\n\t\tName:     newName,\n\t\tSize:     srcObj.GetSize(),\n\t\tIsFolder: srcObj.IsDir(),\n\t\tModified: time.Now(),\n\t}\n\treturn obj, nil\n}\n\nfunc (d *Virtual) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn srcObj, nil\n}\n\nfunc (d *Virtual) Remove(ctx context.Context, obj model.Obj) error {\n\treturn nil\n}\n\nfunc (d *Virtual) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tfile := &model.Object{\n\t\tName:     stream.GetName(),\n\t\tSize:     stream.GetSize(),\n\t\tModified: time.Now(),\n\t}\n\treturn file, nil\n}\n\nvar _ driver.Driver = (*Virtual)(nil)\n"
  },
  {
    "path": "drivers/virtual/meta.go",
    "content": "package virtual\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tNumFile     int   `json:\"num_file\" type:\"number\" default:\"30\" required:\"true\"`\n\tNumFolder   int   `json:\"num_folder\" type:\"number\" default:\"30\" required:\"true\"`\n\tMaxFileSize int64 `json:\"max_file_size\" type:\"number\" default:\"1073741824\" required:\"true\"`\n\tMinFileSize int64 `json:\"min_file_size\"  type:\"number\" default:\"1048576\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:      \"Virtual\",\n\tLocalSort: true,\n\tOnlyProxy: true,\n\tNeedMs:    true,\n\tNoLinkURL: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Virtual{}\n\t})\n}\n"
  },
  {
    "path": "drivers/virtual/util.go",
    "content": "package virtual\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n)\n\nfunc (d *Virtual) genObj(dir bool) model.Obj {\n\tobj := &model.Object{\n\t\tName:     random.String(10),\n\t\tSize:     0,\n\t\tIsFolder: true,\n\t\tModified: time.Now(),\n\t}\n\tif !dir {\n\t\tobj.Size = random.RangeInt64(d.MinFileSize, d.MaxFileSize)\n\t\tobj.IsFolder = false\n\t}\n\treturn obj\n}\n"
  },
  {
    "path": "drivers/webdav/driver.go",
    "content": "package webdav\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/gowebdav\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype WebDav struct {\n\tmodel.Storage\n\tAddition\n\tclient *gowebdav.Client\n\tcron   *cron.Cron\n}\n\nfunc (d *WebDav) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *WebDav) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *WebDav) Init(ctx context.Context) error {\n\terr := d.setClient()\n\tif err == nil {\n\t\td.cron = cron.NewCron(time.Hour * 12)\n\t\td.cron.Do(func() {\n\t\t\t_ = d.setClient()\n\t\t})\n\t}\n\treturn err\n}\n\nfunc (d *WebDav) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\treturn nil\n}\n\nfunc (d *WebDav) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.client.ReadDir(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src os.FileInfo) (model.Obj, error) {\n\t\treturn &model.Object{\n\t\t\tPath:     path.Join(dir.GetPath(), src.Name()),\n\t\t\tName:     src.Name(),\n\t\t\tSize:     src.Size(),\n\t\t\tModified: src.ModTime(),\n\t\t\tIsFolder: src.IsDir(),\n\t\t}, nil\n\t})\n}\n\nfunc (d *WebDav) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\turl, header, err := d.client.Link(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif args.Redirect {\n\t\t// get the url after redirect\n\t\treq := base.NoRedirectClient.R()\n\t\treq.Header = header\n\t\treq.SetDoNotParseResponse(true)\n\t\tres, err := req.Get(url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t_ = res.RawResponse.Body.Close()\n\t\tif (res.StatusCode() == 302 || res.StatusCode() == 307 || res.StatusCode() == 308) && res.Header().Get(\"location\") != \"\" {\n\t\t\turl = res.Header().Get(\"location\")\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"redirect failed, status: %d\", res.StatusCode())\n\t\t}\n\t}\n\treturn &model.Link{\n\t\tURL:    url,\n\t\tHeader: header,\n\t}, nil\n}\n\nfunc (d *WebDav) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName), 0644)\n}\n\nfunc (d *WebDav) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.client.Rename(getPath(srcObj), path.Join(dstDir.GetPath(), srcObj.GetName()), true)\n}\n\nfunc (d *WebDav) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn d.client.Rename(getPath(srcObj), path.Join(path.Dir(srcObj.GetPath()), newName), true)\n}\n\nfunc (d *WebDav) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.client.Copy(getPath(srcObj), path.Join(dstDir.GetPath(), srcObj.GetName()), true)\n}\n\nfunc (d *WebDav) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.client.RemoveAll(getPath(obj))\n}\n\nfunc (d *WebDav) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tcallback := func(r *http.Request) {\n\t\tr.Header.Set(\"Content-Type\", s.GetMimetype())\n\t\tr.ContentLength = s.GetSize()\n\t}\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\terr := d.client.WriteStream(path.Join(dstDir.GetPath(), s.GetName()), reader, 0644, callback)\n\treturn err\n}\n\nvar _ driver.Driver = (*WebDav)(nil)\n"
  },
  {
    "path": "drivers/webdav/meta.go",
    "content": "package webdav\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tVendor   string `json:\"vendor\" type:\"select\" options:\"sharepoint,other\" default:\"other\"`\n\tAddress  string `json:\"address\" required:\"true\"`\n\tUsername string `json:\"username\" required:\"true\"`\n\tPassword string `json:\"password\" required:\"true\"`\n\tdriver.RootPath\n\tTlsInsecureSkipVerify bool `json:\"tls_insecure_skip_verify\" default:\"false\"`\n}\n\nvar config = driver.Config{\n\tName:        \"WebDav\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n\tPreferProxy: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &WebDav{}\n\t})\n}\n"
  },
  {
    "path": "drivers/webdav/odrvcookie/cookie.go",
    "content": "package odrvcookie\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cookie\"\n)\n\n//type SpCookie struct {\n//\tCookie string\n//\texpire time.Time\n//}\n//\n//func (sp SpCookie) IsExpire() bool {\n//\treturn time.Now().After(sp.expire)\n//}\n//\n//var cookiesMap = struct {\n//\tsync.Mutex\n//\tm map[string]*SpCookie\n//}{m: make(map[string]*SpCookie)}\n\nfunc GetCookie(username, password, siteUrl string) (string, error) {\n\t//cookiesMap.Lock()\n\t//defer cookiesMap.Unlock()\n\t//spCookie, ok := cookiesMap.m[username]\n\t//if ok {\n\t//\tif !spCookie.IsExpire() {\n\t//\t\tlog.Debugln(\"sp use old cookie.\")\n\t//\t\treturn spCookie.Cookie, nil\n\t//\t}\n\t//}\n\t//log.Debugln(\"fetch new cookie\")\n\tca := New(username, password, siteUrl)\n\ttokenConf, err := ca.Cookies()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cookie.ToString([]*http.Cookie{&tokenConf.RtFa, &tokenConf.FedAuth}), nil\n\t//spCookie = &SpCookie{\n\t//\tCookie: cookie.ToString([]*http.Cookie{&tokenConf.RtFa, &tokenConf.FedAuth}),\n\t//\texpire: time.Now().Add(time.Hour * 12),\n\t//}\n\t//cookiesMap.m[username] = spCookie\n\t//return spCookie.Cookie, nil\n}\n"
  },
  {
    "path": "drivers/webdav/odrvcookie/fetch.go",
    "content": "// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint\npackage odrvcookie\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// CookieAuth hold the authentication information\n// These are username and password as well as the authentication endpoint\ntype CookieAuth struct {\n\tuser     string\n\tpass     string\n\tendpoint string\n}\n\n// CookieResponse contains the requested cookies\ntype CookieResponse struct {\n\tRtFa    http.Cookie\n\tFedAuth http.Cookie\n}\n\n// SuccessResponse hold a response from the sharepoint webdav\ntype SuccessResponse struct {\n\tXMLName xml.Name            `xml:\"Envelope\"`\n\tSucc    SuccessResponseBody `xml:\"Body\"`\n}\n\n// SuccessResponseBody is the body of a success response, it holds the token\ntype SuccessResponseBody struct {\n\tXMLName xml.Name\n\tType    string    `xml:\"RequestSecurityTokenResponse>TokenType\"`\n\tCreated time.Time `xml:\"RequestSecurityTokenResponse>Lifetime>Created\"`\n\tExpires time.Time `xml:\"RequestSecurityTokenResponse>Lifetime>Expires\"`\n\tToken   string    `xml:\"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken\"`\n}\n\n// reqString is a template that gets populated with the user data in order to retrieve a \"BinarySecurityToken\"\nconst reqString = `<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"\nxmlns:a=\"http://www.w3.org/2005/08/addressing\"\nxmlns:u=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">\n<s:Header>\n<a:Action s:mustUnderstand=\"1\">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>\n<a:ReplyTo>\n<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>\n</a:ReplyTo>\n<a:To s:mustUnderstand=\"1\">{{ .LoginUrl }}</a:To>\n<o:Security s:mustUnderstand=\"1\"\n xmlns:o=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n<o:UsernameToken>\n  <o:Username>{{ .Username }}</o:Username>\n  <o:Password>{{ .Password }}</o:Password>\n</o:UsernameToken>\n</o:Security>\n</s:Header>\n<s:Body>\n<t:RequestSecurityToken xmlns:t=\"http://schemas.xmlsoap.org/ws/2005/02/trust\">\n<wsp:AppliesTo xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\">\n  <a:EndpointReference>\n    <a:Address>{{ .Address }}</a:Address>\n  </a:EndpointReference>\n</wsp:AppliesTo>\n<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>\n<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>\n<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>\n</t:RequestSecurityToken>\n</s:Body>\n</s:Envelope>`\n\n// New creates a new CookieAuth struct\nfunc New(pUser, pPass, pEndpoint string) CookieAuth {\n\tretStruct := CookieAuth{\n\t\tuser:     pUser,\n\t\tpass:     pPass,\n\t\tendpoint: pEndpoint,\n\t}\n\n\treturn retStruct\n}\n\n// Cookies creates a CookieResponse. It fetches the auth token and then\n// retrieves the Cookies\nfunc (ca *CookieAuth) Cookies() (CookieResponse, error) {\n\tspToken, err := ca.getSPToken()\n\tif err != nil {\n\t\treturn CookieResponse{}, err\n\t}\n\treturn ca.getSPCookie(spToken)\n}\n\nfunc (ca *CookieAuth) getSPCookie(conf *SuccessResponse) (CookieResponse, error) {\n\tspRoot, err := url.Parse(ca.endpoint)\n\tif err != nil {\n\t\treturn CookieResponse{}, err\n\t}\n\n\tu, err := url.Parse(\"https://\" + spRoot.Host + \"/_forms/default.aspx?wa=wsignin1.0\")\n\tif err != nil {\n\t\treturn CookieResponse{}, err\n\t}\n\n\t// To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth)\n\t// In order to get them we use the token we got earlier and a cookieJar\n\tjar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})\n\tif err != nil {\n\t\treturn CookieResponse{}, err\n\t}\n\n\tclient := &http.Client{\n\t\tJar: jar,\n\t}\n\n\t// Send the previously acquired Token as a Post parameter\n\tif _, err = client.Post(u.String(), \"text/xml\", strings.NewReader(conf.Succ.Token)); err != nil {\n\t\treturn CookieResponse{}, err\n\t}\n\n\tcookieResponse := CookieResponse{}\n\tfor _, cookie := range jar.Cookies(u) {\n\t\tif (cookie.Name == \"rtFa\") || (cookie.Name == \"FedAuth\") {\n\t\t\tswitch cookie.Name {\n\t\t\tcase \"rtFa\":\n\t\t\t\tcookieResponse.RtFa = *cookie\n\t\t\tcase \"FedAuth\":\n\t\t\t\tcookieResponse.FedAuth = *cookie\n\t\t\t}\n\t\t}\n\t}\n\treturn cookieResponse, err\n}\n\nvar loginUrlsMap = map[string]string{\n\t\"com\": \"https://login.microsoftonline.com\",\n\t\"cn\":  \"https://login.chinacloudapi.cn\",\n\t\"us\":  \"https://login.microsoftonline.us\",\n\t\"de\":  \"https://login.microsoftonline.de\",\n}\n\nfunc getLoginUrl(endpoint string) (string, error) {\n\tspRoot, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdomains := strings.Split(spRoot.Host, \".\")\n\ttld := domains[len(domains)-1]\n\tloginUrl, ok := loginUrlsMap[tld]\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"tld %s is not supported\", tld)\n\t}\n\treturn loginUrl + \"/extSTS.srf\", nil\n}\n\nfunc (ca *CookieAuth) getSPToken() (*SuccessResponse, error) {\n\tloginUrl, err := getLoginUrl(ca.endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treqData := map[string]string{\n\t\t\"Username\": ca.user,\n\t\t\"Password\": ca.pass,\n\t\t\"Address\":  ca.endpoint,\n\t\t\"LoginUrl\": loginUrl,\n\t}\n\n\tt := template.Must(template.New(\"authXML\").Parse(reqString))\n\n\tbuf := &bytes.Buffer{}\n\tif err := t.Execute(buf, reqData); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Execute the first request which gives us an auth token for the sharepoint service\n\t// With this token we can authenticate on the login page and save the returned cookies\n\treq, err := http.NewRequest(http.MethodPost, loginUrl, buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := base.HttpClient\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBuf := bytes.Buffer{}\n\trespBuf.ReadFrom(resp.Body)\n\ts := respBuf.Bytes()\n\n\tvar conf SuccessResponse\n\terr = xml.Unmarshal(s, &conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &conf, err\n}\n"
  },
  {
    "path": "drivers/webdav/types.go",
    "content": "package webdav\n"
  },
  {
    "path": "drivers/webdav/util.go",
    "content": "package webdav\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/webdav/odrvcookie\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/gowebdav\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *WebDav) isSharepoint() bool {\n\treturn d.Vendor == \"sharepoint\"\n}\n\nfunc (d *WebDav) setClient() error {\n\tc := gowebdav.NewClient(d.Address, d.Username, d.Password)\n\tc.SetTransport(&http.Transport{\n\t\tProxy:           http.ProxyFromEnvironment,\n\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: d.TlsInsecureSkipVerify},\n\t})\n\tif d.isSharepoint() {\n\t\tcookie, err := odrvcookie.GetCookie(d.Username, d.Password, d.Address)\n\t\tif err == nil {\n\t\t\tc.SetInterceptor(func(method string, rq *http.Request) {\n\t\t\t\trq.Header.Del(\"Authorization\")\n\t\t\t\trq.Header.Set(\"Cookie\", cookie)\n\t\t\t})\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tcookieJar, err := cookiejar.New(nil)\n\t\tif err == nil {\n\t\t\tc.SetJar(cookieJar)\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\td.client = c\n\treturn nil\n}\n\nfunc getPath(obj model.Obj) string {\n\tif obj.IsDir() {\n\t\treturn obj.GetPath() + \"/\"\n\t}\n\treturn obj.GetPath()\n}\n"
  },
  {
    "path": "drivers/weiyun/driver.go",
    "content": "package weiyun\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/errgroup\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\tweiyunsdkgo \"github.com/foxxorcat/weiyun-sdk-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype WeiYun struct {\n\tmodel.Storage\n\tAddition\n\n\tclient     *weiyunsdkgo.WeiYunClient\n\tcron       *cron.Cron\n\trootFolder *Folder\n\n\tuploadThread int\n}\n\nfunc (d *WeiYun) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *WeiYun) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *WeiYun) Init(ctx context.Context) error {\n\t// 限制上传线程数\n\td.uploadThread, _ = strconv.Atoi(d.UploadThread)\n\tif d.uploadThread < 4 || d.uploadThread > 32 {\n\t\td.uploadThread, d.UploadThread = 4, \"4\"\n\t}\n\n\td.client = weiyunsdkgo.NewWeiYunClientWithRestyClient(base.NewRestyClient())\n\terr := d.client.SetCookiesStr(d.Cookies).RefreshCtoken()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Cookie过期回调\n\td.client.SetOnCookieExpired(func(err error) {\n\t\td.Status = err.Error()\n\t\top.MustSaveDriverStorage(d)\n\t})\n\n\t// cookie更新回调\n\td.client.SetOnCookieUpload(func(c []*http.Cookie) {\n\t\td.Cookies = weiyunsdkgo.CookieToString(weiyunsdkgo.ClearCookie(c))\n\t\top.MustSaveDriverStorage(d)\n\t})\n\n\t// qqCookie保活\n\tif d.client.LoginType() == weiyunsdkgo.AccountTypeQQ || d.client.LoginType() == weiyunsdkgo.AccountTypeQQOpenID {\n\t\td.cron = cron.NewCron(time.Minute * 5)\n\t\td.cron.Do(func() {\n\t\t\t_ = d.client.KeepAlive()\n\t\t})\n\t}\n\n\t// 获取默认根目录dirKey\n\tif d.RootFolderID == \"\" {\n\t\tuserInfo, err := d.client.DiskUserInfoGet()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.RootFolderID = userInfo.MainDirKey\n\t}\n\n\t// 处理目录ID，找到PdirKey\n\tfolders, err := d.client.LibDirPathGet(d.RootFolderID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(folders) == 0 {\n\t\treturn fmt.Errorf(\"invalid directory ID\")\n\t}\n\n\tfolder := folders[len(folders)-1]\n\td.rootFolder = &Folder{\n\t\tPFolder: &Folder{\n\t\t\tFolder: weiyunsdkgo.Folder{\n\t\t\t\tDirKey: folder.PdirKey,\n\t\t\t},\n\t\t},\n\t\tFolder: folder.Folder,\n\t}\n\treturn nil\n}\n\nfunc (d *WeiYun) Drop(ctx context.Context) error {\n\td.client = nil\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t\td.cron = nil\n\t}\n\treturn nil\n}\n\nfunc (d *WeiYun) GetRoot(ctx context.Context) (model.Obj, error) {\n\treturn d.rootFolder, nil\n}\n\nfunc (d *WeiYun) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif folder, ok := dir.(*Folder); ok {\n\t\tvar files []model.Obj\n\t\tfor {\n\t\t\tdata, err := d.client.DiskDirFileList(folder.GetID(), weiyunsdkgo.WarpParamOption(\n\t\t\t\tweiyunsdkgo.QueryFileOptionOffest(int64(len(files))),\n\t\t\t\tweiyunsdkgo.QueryFileOptionGetType(weiyunsdkgo.FileAndDir),\n\t\t\t\tweiyunsdkgo.QueryFileOptionSort(func() weiyunsdkgo.OrderBy {\n\t\t\t\t\tswitch d.OrderBy {\n\t\t\t\t\tcase \"name\":\n\t\t\t\t\t\treturn weiyunsdkgo.FileName\n\t\t\t\t\tcase \"size\":\n\t\t\t\t\t\treturn weiyunsdkgo.FileSize\n\t\t\t\t\tcase \"updated_at\":\n\t\t\t\t\t\treturn weiyunsdkgo.FileMtime\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn weiyunsdkgo.FileName\n\t\t\t\t\t}\n\t\t\t\t}(), d.OrderDirection == \"desc\"),\n\t\t\t))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif files == nil {\n\t\t\t\tfiles = make([]model.Obj, 0, data.TotalDirCount+data.TotalFileCount)\n\t\t\t}\n\n\t\t\tfor _, dir := range data.DirList {\n\t\t\t\tfiles = append(files, &Folder{\n\t\t\t\t\tPFolder: folder,\n\t\t\t\t\tFolder:  dir,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfor _, file := range data.FileList {\n\t\t\t\tfiles = append(files, &File{\n\t\t\t\t\tPFolder: folder,\n\t\t\t\t\tFile:    file,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif data.FinishFlag || len(data.DirList)+len(data.FileList) == 0 {\n\t\t\t\treturn files, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *WeiYun) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file, ok := file.(*File); ok {\n\t\tdata, err := d.client.DiskFileDownload(weiyunsdkgo.FileParam{PdirKey: file.GetPKey(), FileID: file.GetID()})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &model.Link{\n\t\t\tURL: data.DownloadUrl,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Cookie\": []string{data.CookieName + \"=\" + data.CookieValue},\n\t\t\t},\n\t\t}, nil\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *WeiYun) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif folder, ok := parentDir.(*Folder); ok {\n\t\tnewFolder, err := d.client.DiskDirCreate(weiyunsdkgo.FolderParam{\n\t\t\tPPdirKey: folder.GetPKey(),\n\t\t\tPdirKey:  folder.DirKey,\n\t\t\tDirName:  dirName,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &Folder{\n\t\t\tPFolder: folder,\n\t\t\tFolder:  *newFolder,\n\t\t}, nil\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *WeiYun) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// TODO: 默认策略为重命名，使用缓存可能出现冲突。微云app也有这个冲突，不知道腾讯怎么搞的\n\tif dstDir, ok := dstDir.(*Folder); ok {\n\t\tdstParam := weiyunsdkgo.FolderParam{\n\t\t\tPdirKey: dstDir.GetPKey(),\n\t\t\tDirKey:  dstDir.GetID(),\n\t\t\tDirName: dstDir.GetName(),\n\t\t}\n\t\tswitch srcObj := srcObj.(type) {\n\t\tcase *File:\n\t\t\terr := d.client.DiskFileMove(weiyunsdkgo.FileParam{\n\t\t\t\tPPdirKey: srcObj.PFolder.GetPKey(),\n\t\t\t\tPdirKey:  srcObj.GetPKey(),\n\t\t\t\tFileID:   srcObj.GetID(),\n\t\t\t\tFileName: srcObj.GetName(),\n\t\t\t}, dstParam)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &File{\n\t\t\t\tPFolder: dstDir,\n\t\t\t\tFile:    srcObj.File,\n\t\t\t}, nil\n\t\tcase *Folder:\n\t\t\terr := d.client.DiskDirMove(weiyunsdkgo.FolderParam{\n\t\t\t\tPPdirKey: srcObj.PFolder.GetPKey(),\n\t\t\t\tPdirKey:  srcObj.GetPKey(),\n\t\t\t\tDirKey:   srcObj.GetID(),\n\t\t\t\tDirName:  srcObj.GetName(),\n\t\t\t}, dstParam)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &Folder{\n\t\t\t\tPFolder: dstDir,\n\t\t\t\tFolder:  srcObj.Folder,\n\t\t\t}, nil\n\t\t}\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *WeiYun) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tswitch srcObj := srcObj.(type) {\n\tcase *File:\n\t\terr := d.client.DiskFileRename(weiyunsdkgo.FileParam{\n\t\t\tPPdirKey: srcObj.PFolder.GetPKey(),\n\t\t\tPdirKey:  srcObj.GetPKey(),\n\t\t\tFileID:   srcObj.GetID(),\n\t\t\tFileName: srcObj.GetName(),\n\t\t}, newName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnewFile := srcObj.File\n\t\tnewFile.FileName = newName\n\t\tnewFile.FileCtime = weiyunsdkgo.TimeStamp(time.Now())\n\t\treturn &File{\n\t\t\tPFolder: srcObj.PFolder,\n\t\t\tFile:    newFile,\n\t\t}, nil\n\tcase *Folder:\n\t\terr := d.client.DiskDirAttrModify(weiyunsdkgo.FolderParam{\n\t\t\tPPdirKey: srcObj.PFolder.GetPKey(),\n\t\t\tPdirKey:  srcObj.GetPKey(),\n\t\t\tDirKey:   srcObj.GetID(),\n\t\t\tDirName:  srcObj.GetName(),\n\t\t}, newName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnewFolder := srcObj.Folder\n\t\tnewFolder.DirName = newName\n\t\tnewFolder.DirCtime = weiyunsdkgo.TimeStamp(time.Now())\n\t\treturn &Folder{\n\t\t\tPFolder: srcObj.PFolder,\n\t\t\tFolder:  newFolder,\n\t\t}, nil\n\t}\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *WeiYun) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *WeiYun) Remove(ctx context.Context, obj model.Obj) error {\n\tswitch obj := obj.(type) {\n\tcase *File:\n\t\treturn d.client.DiskFileDelete(weiyunsdkgo.FileParam{\n\t\t\tPPdirKey: obj.PFolder.GetPKey(),\n\t\t\tPdirKey:  obj.GetPKey(),\n\t\t\tFileID:   obj.GetID(),\n\t\t\tFileName: obj.GetName(),\n\t\t})\n\tcase *Folder:\n\t\treturn d.client.DiskDirDelete(weiyunsdkgo.FolderParam{\n\t\t\tPPdirKey: obj.PFolder.GetPKey(),\n\t\t\tPdirKey:  obj.GetPKey(),\n\t\t\tDirKey:   obj.GetID(),\n\t\t\tDirName:  obj.GetName(),\n\t\t})\n\t}\n\treturn errs.NotSupport\n}\n\nfunc (d *WeiYun) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// NOTE:\n\t// 秒传需要sha1最后一个状态,但sha1无法逆运算需要读完整个文件(或许可以??)\n\t// 服务器支持上传进度恢复,不需要额外实现\n\tvar folder *Folder\n\tvar ok bool\n\tif folder, ok = dstDir.(*Folder); !ok {\n\t\treturn nil, errs.NotSupport\n\t}\n\tfile, err := stream.CacheFullAndWriter(&up, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// step 1.\n\tpreData, err := d.client.PreUpload(ctx, weiyunsdkgo.UpdloadFileParam{\n\t\tPdirKey: folder.GetPKey(),\n\t\tDirKey:  folder.DirKey,\n\n\t\tFileName: stream.GetName(),\n\t\tFileSize: stream.GetSize(),\n\t\tFile:     file,\n\n\t\tChannelCount:    4,\n\t\tFileExistOption: 1,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// not fast upload\n\tif !preData.FileExist {\n\t\t// step.2 增加上传通道\n\t\tif len(preData.ChannelList) < d.uploadThread {\n\t\t\tnewCh, err := d.client.AddUploadChannel(len(preData.ChannelList), d.uploadThread, preData.UploadAuthData)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpreData.ChannelList = append(preData.ChannelList, newCh.AddChannels...)\n\t\t}\n\t\t// step.3 上传\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, len(preData.ChannelList),\n\t\t\tretry.Attempts(3),\n\t\t\tretry.Delay(time.Second),\n\t\t\tretry.DelayType(retry.BackOffDelay))\n\n\t\ttotal := atomic.Int64{}\n\t\tfor _, channel := range preData.ChannelList {\n\t\t\tif utils.IsCanceled(upCtx) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tvar channel = channel\n\t\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t\tfor {\n\t\t\t\t\tchannel.Len = int(math.Min(float64(stream.GetSize()-channel.Offset), float64(channel.Len)))\n\t\t\t\t\tlen64 := int64(channel.Len)\n\t\t\t\t\tupData, err := d.client.UploadFile(upCtx, channel, preData.UploadAuthData,\n\t\t\t\t\t\tdriver.NewLimitedUploadStream(ctx, io.NewSectionReader(file, channel.Offset, len64)))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcur := total.Add(len64)\n\t\t\t\t\tup(float64(cur) * 100.0 / float64(stream.GetSize()))\n\t\t\t\t\t// 上传完成\n\t\t\t\t\tif upData.UploadState != 1 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tchannel = upData.Channel\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\tif err = threadG.Wait(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &File{\n\t\tPFolder: folder,\n\t\tFile:    preData.File,\n\t}, nil\n}\n\nfunc (d *WeiYun) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tinfo, err := d.client.DiskUserInfoGet(func(request *resty.Request) {\n\t\trequest.SetContext(ctx)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: info.TotalSpace,\n\t\t\tUsedSpace:  info.UsedSpace,\n\t\t},\n\t}, nil\n}\n\n// func (d *WeiYun) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n// \treturn nil, errs.NotSupport\n// }\n\nvar _ driver.Driver = (*WeiYun)(nil)\nvar _ driver.GetRooter = (*WeiYun)(nil)\nvar _ driver.MkdirResult = (*WeiYun)(nil)\n\n// var _ driver.CopyResult = (*WeiYun)(nil)\nvar _ driver.MoveResult = (*WeiYun)(nil)\nvar _ driver.Remove = (*WeiYun)(nil)\n\nvar _ driver.PutResult = (*WeiYun)(nil)\nvar _ driver.RenameResult = (*WeiYun)(nil)\nvar _ driver.WithDetails = (*WeiYun)(nil)\n"
  },
  {
    "path": "drivers/weiyun/meta.go",
    "content": "package weiyun\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tRootFolderID   string `json:\"root_folder_id\"`\n\tCookies        string `json:\"cookies\" required:\"true\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"name,size,updated_at\" default:\"name\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tUploadThread   string `json:\"upload_thread\" default:\"4\" help:\"4<=thread<=32\"`\n}\n\nvar config = driver.Config{\n\tName:        \"WeiYun\",\n\tOnlyProxy:   true,\n\tCheckStatus: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &WeiYun{}\n\t})\n}\n"
  },
  {
    "path": "drivers/weiyun/types.go",
    "content": "package weiyun\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\n\tweiyunsdkgo \"github.com/foxxorcat/weiyun-sdk-go\"\n)\n\ntype File struct {\n\tPFolder *Folder\n\tweiyunsdkgo.File\n}\n\nfunc (f *File) GetID() string      { return f.FileID }\nfunc (f *File) GetSize() int64     { return f.FileSize }\nfunc (f *File) GetName() string    { return f.FileName }\nfunc (f *File) ModTime() time.Time { return time.Time(f.FileMtime) }\nfunc (f *File) IsDir() bool        { return false }\nfunc (f *File) GetPath() string    { return \"\" }\n\nfunc (f *File) GetPKey() string {\n\treturn f.PFolder.DirKey\n}\nfunc (f *File) CreateTime() time.Time {\n\treturn time.Time(f.FileCtime)\n}\n\nfunc (f *File) GetHash() utils.HashInfo {\n\treturn utils.NewHashInfo(utils.SHA1, f.FileSha)\n}\n\ntype Folder struct {\n\tPFolder *Folder\n\tweiyunsdkgo.Folder\n}\n\nfunc (f *Folder) CreateTime() time.Time {\n\treturn time.Time(f.DirCtime)\n}\n\nfunc (f *Folder) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\n}\n\nfunc (f *Folder) GetID() string      { return f.DirKey }\nfunc (f *Folder) GetSize() int64     { return 0 }\nfunc (f *Folder) GetName() string    { return f.DirName }\nfunc (f *Folder) ModTime() time.Time { return time.Time(f.DirMtime) }\nfunc (f *Folder) IsDir() bool        { return true }\nfunc (f *Folder) GetPath() string    { return \"\" }\n\nfunc (f *Folder) GetPKey() string {\n\treturn f.PFolder.DirKey\n}\n"
  },
  {
    "path": "drivers/wopan/driver.go",
    "content": "package template\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/wopan-sdk-go\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Wopan struct {\n\tmodel.Storage\n\tAddition\n\tclient          *wopan.WoClient\n\tdefaultFamilyID string\n}\n\nfunc (d *Wopan) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Wopan) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Wopan) Init(ctx context.Context) error {\n\td.client = wopan.DefaultWithRefreshToken(d.RefreshToken)\n\td.client.SetAccessToken(d.AccessToken)\n\td.client.OnRefreshToken(func(accessToken, refreshToken string) {\n\t\td.AccessToken = accessToken\n\t\td.RefreshToken = refreshToken\n\t\top.MustSaveDriverStorage(d)\n\t})\n\tfml, err := d.client.FamilyUserCurrentEncode()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.defaultFamilyID = strconv.Itoa(fml.DefaultHomeId)\n\treturn d.client.InitData()\n}\n\nfunc (d *Wopan) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Wopan) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar res []model.Obj\n\tpageNum := 0\n\tpageSize := 100\n\tfor {\n\t\tdata, err := d.client.QueryAllFiles(d.getSpaceType(), dir.GetID(), pageNum, pageSize, 0, d.FamilyID, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tobjs, err := utils.SliceConvert(data.Files, fileToObj)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, objs...)\n\t\tif len(data.Files) < pageSize {\n\t\t\tbreak\n\t\t}\n\t\tpageNum++\n\t}\n\treturn res, nil\n}\n\nfunc (d *Wopan) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif f, ok := file.(*Object); ok {\n\t\tres, err := d.client.GetDownloadUrlV2([]string{f.FID}, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &model.Link{\n\t\t\tURL: res.List[0].DownloadUrl,\n\t\t}, nil\n\t}\n\treturn nil, fmt.Errorf(\"unable to convert file to Object\")\n}\n\nfunc (d *Wopan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tfamilyID := d.FamilyID\n\tif familyID == \"\" {\n\t\tfamilyID = d.defaultFamilyID\n\t}\n\t_, err := d.client.CreateDirectory(d.getSpaceType(), parentDir.GetID(), dirName, familyID, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t})\n\treturn err\n}\n\nfunc (d *Wopan) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdirList := make([]string, 0)\n\tfileList := make([]string, 0)\n\tif srcObj.IsDir() {\n\t\tdirList = append(dirList, srcObj.GetID())\n\t} else {\n\t\tfileList = append(fileList, srcObj.GetID())\n\t}\n\treturn d.client.MoveFile(dirList, fileList, dstDir.GetID(),\n\t\td.getSpaceType(), d.getSpaceType(),\n\t\td.FamilyID, d.FamilyID, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t})\n}\n\nfunc (d *Wopan) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_type := 1\n\tif srcObj.IsDir() {\n\t\t_type = 0\n\t}\n\treturn d.client.RenameFileOrDirectory(d.getSpaceType(), _type, srcObj.GetID(), newName, d.FamilyID, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t})\n}\n\nfunc (d *Wopan) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tdirList := make([]string, 0)\n\tfileList := make([]string, 0)\n\tif srcObj.IsDir() {\n\t\tdirList = append(dirList, srcObj.GetID())\n\t} else {\n\t\tfileList = append(fileList, srcObj.GetID())\n\t}\n\treturn d.client.CopyFile(dirList, fileList, dstDir.GetID(),\n\t\td.getSpaceType(), d.getSpaceType(),\n\t\td.FamilyID, d.FamilyID, func(req *resty.Request) {\n\t\t\treq.SetContext(ctx)\n\t\t})\n}\n\nfunc (d *Wopan) Remove(ctx context.Context, obj model.Obj) error {\n\tdirList := make([]string, 0)\n\tfileList := make([]string, 0)\n\tif obj.IsDir() {\n\t\tdirList = append(dirList, obj.GetID())\n\t} else {\n\t\tfileList = append(fileList, obj.GetID())\n\t}\n\treturn d.client.DeleteFile(d.getSpaceType(), dirList, fileList, func(req *resty.Request) {\n\t\treq.SetContext(ctx)\n\t})\n}\n\nfunc (d *Wopan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\t_, err := d.client.Upload2C(d.getSpaceType(), wopan.Upload2CFile{\n\t\tName:        stream.GetName(),\n\t\tSize:        stream.GetSize(),\n\t\tContent:     driver.NewLimitedUploadStream(ctx, stream),\n\t\tContentType: stream.GetMimetype(),\n\t}, dstDir.GetID(), d.FamilyID, wopan.Upload2COption{\n\t\tOnProgress: func(current, total int64) {\n\t\t\tup(100 * float64(current) / float64(total))\n\t\t},\n\t\tCtx: ctx,\n\t})\n\treturn err\n}\n\nfunc (d *Wopan) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tquota, err := d.client.QueryCloudUsageInfo()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := strconv.ParseInt(quota.UsageInfo.ByteTotalSize, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: total,\n\t\t\tUsedSpace:  quota.UsageInfo.ByteUsedSize,\n\t\t},\n\t}, nil\n}\n\n//func (d *Wopan) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Wopan)(nil)\n"
  },
  {
    "path": "drivers/wopan/meta.go",
    "content": "package template\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootID\n\t// define other\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\n\tFamilyID     string `json:\"family_id\" help:\"Keep it empty if you want to use your personal drive\"`\n\tSortRule     string `json:\"sort_rule\" type:\"select\" options:\"name_asc,name_desc,time_asc,time_desc,size_asc,size_desc\" default:\"name_asc\"`\n\n\tAccessToken string `json:\"access_token\"`\n}\n\nvar config = driver.Config{\n\tName:              \"WoPan\",\n\tDefaultRoot:       \"0\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Wopan{}\n\t})\n}\n"
  },
  {
    "path": "drivers/wopan/types.go",
    "content": "package template\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/wopan-sdk-go\"\n)\n\ntype Object struct {\n\tmodel.ObjThumb\n\tFID string\n}\n\nfunc fileToObj(file wopan.File) (model.Obj, error) {\n\tt, err := getTime(file.CreateTime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Object{\n\t\tObjThumb: model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tID: file.Id,\n\t\t\t\t//Path:     \"\",\n\t\t\t\tName:     file.Name,\n\t\t\t\tSize:     file.Size,\n\t\t\t\tModified: t,\n\t\t\t\tIsFolder: file.Type == 0,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{\n\t\t\t\tThumbnail: file.ThumbUrl,\n\t\t\t},\n\t\t},\n\t\tFID: file.Fid,\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/wopan/util.go",
    "content": "package template\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/wopan-sdk-go\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *Wopan) getSortRule() int {\n\tswitch d.SortRule {\n\tcase \"name_asc\":\n\t\treturn wopan.SortNameAsc\n\tcase \"name_desc\":\n\t\treturn wopan.SortNameDesc\n\tcase \"time_asc\":\n\t\treturn wopan.SortTimeAsc\n\tcase \"time_desc\":\n\t\treturn wopan.SortTimeDesc\n\tcase \"size_asc\":\n\t\treturn wopan.SortSizeAsc\n\tcase \"size_desc\":\n\t\treturn wopan.SortSizeDesc\n\tdefault:\n\t\treturn wopan.SortNameAsc\n\t}\n}\n\nfunc (d *Wopan) getSpaceType() string {\n\tif d.FamilyID == \"\" {\n\t\treturn wopan.SpaceTypePersonal\n\t}\n\treturn wopan.SpaceTypeFamily\n}\n\n// 20230607214351\nfunc getTime(str string) (time.Time, error) {\n\tloc := time.FixedZone(\"UTC+8\", 8*60*60)\n\treturn time.ParseInLocation(\"20060102150405\", str, loc)\n}\n"
  },
  {
    "path": "drivers/wps/driver.go",
    "content": "package wps\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Wps struct {\n\tmodel.Storage\n\tAddition\n\tcompanyID string\n}\n\nfunc (d *Wps) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Wps) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Wps) Init(ctx context.Context) error {\n\tif d.Cookie == \"\" {\n\t\treturn fmt.Errorf(\"cookie is empty\")\n\t}\n\treturn d.ensureCompanyID(ctx)\n}\n\nfunc (d *Wps) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Wps) List(ctx context.Context, dir model.Obj, _ model.ListArgs) ([]model.Obj, error) {\n\tbasePath := \"/\"\n\tif dir != nil {\n\t\tif p := dir.GetPath(); p != \"\" {\n\t\t\tbasePath = p\n\t\t}\n\t}\n\treturn d.list(ctx, basePath)\n}\n\nfunc (d *Wps) Link(ctx context.Context, file model.Obj, _ model.LinkArgs) (*model.Link, error) {\n\tif file == nil {\n\t\treturn nil, errs.NotSupport\n\t}\n\treturn d.link(ctx, file.GetPath())\n}\n\nfunc (d *Wps) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn d.makeDir(ctx, parentDir, dirName)\n}\n\nfunc (d *Wps) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.move(ctx, srcObj, dstDir)\n}\n\nfunc (d *Wps) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn d.rename(ctx, srcObj, newName)\n}\n\nfunc (d *Wps) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn d.copy(ctx, srcObj, dstDir)\n}\n\nfunc (d *Wps) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.remove(ctx, obj)\n}\n\nfunc (d *Wps) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\treturn d.put(ctx, dstDir, file, up)\n}\n\nfunc (d *Wps) GetDetails(ctx context.Context) (*model.StorageDetails, error) {\n\tquota, err := d.spaces(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.StorageDetails{\n\t\tDiskUsage: model.DiskUsage{\n\t\t\tTotalSpace: quota.Total,\n\t\t\tUsedSpace:  quota.Used,\n\t\t},\n\t}, nil\n}\n\nvar _ driver.Driver = (*Wps)(nil)\n"
  },
  {
    "path": "drivers/wps/meta.go",
    "content": "package wps\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tCookie string `json:\"cookie\" required:\"true\" type:\"text\"`\n\tMode   string `json:\"mode\" type:\"select\" options:\"Personal,Business\" default:\"Business\"`\n}\n\nvar config = driver.Config{\n\tName:              \"WPS\",\n\tLocalSort:         true,\n\tDefaultRoot:       \"/\",\n\tAlert:             \"\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Wps{}\n\t})\n}\n"
  },
  {
    "path": "drivers/wps/types.go",
    "content": "package wps\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype workspaceResp struct {\n\tCompanies []struct {\n\t\tID int64 `json:\"id\"`\n\t} `json:\"companies\"`\n}\n\ntype Group struct {\n\tCompanyID int64  `json:\"company_id\"`\n\tGroupID   int64  `json:\"group_id\"`\n\tName      string `json:\"name\"`\n\tType      string `json:\"type\"`\n}\n\ntype groupsResp struct {\n\tGroups []Group `json:\"groups\"`\n}\n\ntype filePerms struct {\n\tDownload int `json:\"download\"`\n}\n\ntype FileInfo struct {\n\tGroupID   int64     `json:\"groupid\"`\n\tParentID  int64     `json:\"parentid\"`\n\tName      string    `json:\"fname\"`\n\tSize      int64     `json:\"fsize\"`\n\tType      string    `json:\"ftype\"`\n\tCtime     int64     `json:\"ctime\"`\n\tMtime     int64     `json:\"mtime\"`\n\tID        int64     `json:\"id\"`\n\tDeleted   bool      `json:\"deleted\"`\n\tFilePerms filePerms `json:\"file_perms_acl\"`\n}\n\ntype filesResp struct {\n\tFiles      []FileInfo `json:\"files\"`\n\tNextOffset int        `json:\"next_offset\"`\n}\n\ntype downloadResp struct {\n\tURL    string `json:\"url\"`\n\tResult string `json:\"result\"`\n}\n\ntype spacesResp struct {\n\tId        int64  `json:\"id\"`\n\tName      string `json:\"name\"`\n\tResult    string `json:\"result\"`\n\tTotal     int64  `json:\"total\"`\n\tUsed      int64  `json:\"used\"`\n\tUsedParts []struct {\n\t\tType string `json:\"type\"`\n\t\tUsed int64  `json:\"used\"`\n\t} `json:\"used_parts\"`\n}\n\ntype Obj struct {\n\tid          string\n\tname        string\n\tsize        int64\n\tctime       time.Time\n\tmtime       time.Time\n\tisDir       bool\n\thash        utils.HashInfo\n\tpath        string\n\tcanDownload bool\n}\n\nfunc (o *Obj) GetSize() int64 {\n\treturn o.size\n}\n\nfunc (o *Obj) GetName() string {\n\treturn o.name\n}\n\nfunc (o *Obj) ModTime() time.Time {\n\treturn o.mtime\n}\n\nfunc (o *Obj) CreateTime() time.Time {\n\treturn o.ctime\n}\n\nfunc (o *Obj) IsDir() bool {\n\treturn o.isDir\n}\n\nfunc (o *Obj) GetHash() utils.HashInfo {\n\treturn o.hash\n}\n\nfunc (o *Obj) GetID() string {\n\treturn o.id\n}\n\nfunc (o *Obj) GetPath() string {\n\treturn o.path\n}\n"
  },
  {
    "path": "drivers/wps/util.go",
    "content": "package wps\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://365.kdocs.cn\"\nconst personalEndpoint = \"https://drive.wps.cn\"\n\ntype resolvedNode struct {\n\tkind  string\n\tgroup Group\n\tfile  *FileInfo\n}\n\ntype resolveCacheEntry struct {\n\tnode   *resolvedNode\n\texpire time.Time\n}\n\ntype resolveCacheStore struct {\n\tmu sync.RWMutex\n\tm  map[string]resolveCacheEntry\n}\n\nvar resolveCaches sync.Map\n\ntype apiResult struct {\n\tResult string `json:\"result\"`\n\tMsg    string `json:\"msg\"`\n}\n\ntype uploadCreateUpdateResp struct {\n\tapiResult\n\tMethod  string `json:\"method\"`\n\tURL     string `json:\"url\"`\n\tStore   string `json:\"store\"`\n\tRequest struct {\n\t\tHeaders  map[string]string `json:\"headers\"`\n\t\tFormData map[string]string `json:\"formData\"`\n\t} `json:\"request\"`\n\tResponse struct {\n\t\tExpectCode []int  `json:\"expect_code\"`\n\t\tArgsETag   string `json:\"args_etag\"`\n\t\tArgsKey    string `json:\"args_key\"`\n\t} `json:\"response\"`\n}\n\ntype uploadPutResp struct {\n\tNewFilename string `json:\"newfilename\"`\n\tSha1        string `json:\"sha1\"`\n\tMD5         string `json:\"md5\"`\n}\n\ntype personalGroupsResp struct {\n\tapiResult\n\tGroups []struct {\n\t\tID   int64  `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t} `json:\"groups\"`\n}\n\ntype countingWriter struct {\n\tn *int64\n}\n\nfunc (w countingWriter) Write(p []byte) (int, error) {\n\t*w.n += int64(len(p))\n\treturn len(p), nil\n}\n\nfunc (d *Wps) isPersonal() bool {\n\treturn strings.TrimSpace(d.Mode) == \"Personal\"\n}\n\nfunc (d *Wps) driveHost() string {\n\tif d.isPersonal() {\n\t\treturn personalEndpoint\n\t}\n\treturn endpoint\n}\n\nfunc (d *Wps) drivePrefix() string {\n\tif d.isPersonal() {\n\t\treturn \"\"\n\t}\n\treturn \"/3rd/drive\"\n}\n\nfunc (d *Wps) driveURL(path string) string {\n\treturn d.driveHost() + d.drivePrefix() + path\n}\n\nfunc (d *Wps) origin() string {\n\treturn d.driveHost()\n}\n\nfunc (d *Wps) canDownload(f *FileInfo) bool {\n\tif f == nil || f.Type == \"folder\" {\n\t\treturn false\n\t}\n\tif f.FilePerms.Download != 0 {\n\t\treturn true\n\t}\n\treturn d.isPersonal()\n}\n\nfunc (d *Wps) request(ctx context.Context) *resty.Request {\n\treturn base.RestyClient.R().\n\t\tSetHeader(\"Cookie\", d.Cookie).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetContext(ctx)\n}\n\nfunc (d *Wps) jsonRequest(ctx context.Context) *resty.Request {\n\treturn d.request(ctx).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"Origin\", d.origin())\n}\n\nfunc statusOK(code int, expect []int) bool {\n\tif len(expect) == 0 {\n\t\treturn code >= 200 && code < 300\n\t}\n\tfor _, v := range expect {\n\t\tif v == code {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc respArg(arg string, resp *http.Response, body []byte) string {\n\targ = strings.TrimSpace(arg)\n\tif arg == \"\" {\n\t\treturn \"\"\n\t}\n\tl := strings.ToLower(arg)\n\tif strings.HasPrefix(l, \"header.\") {\n\t\th := strings.TrimSpace(arg[len(\"header.\"):])\n\t\tif h == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn strings.TrimSpace(resp.Header.Get(h))\n\t}\n\tif strings.HasPrefix(l, \"body.\") {\n\t\tk := strings.TrimSpace(arg[len(\"body.\"):])\n\t\tif k == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\tvar m map[string]interface{}\n\t\tif err := json.Unmarshal(body, &m); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tif v, ok := m[k]; ok {\n\t\t\tif s, ok := v.(string); ok {\n\t\t\t\treturn strings.TrimSpace(s)\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractXMLTag(v, tag string) string {\n\ts := strings.TrimSpace(v)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tlt := strings.ToLower(tag)\n\topen := \"<\" + lt + \">\"\n\tclos := \"</\" + lt + \">\"\n\tls := strings.ToLower(s)\n\ti := strings.Index(ls, open)\n\tif i < 0 {\n\t\treturn \"\"\n\t}\n\ti += len(open)\n\tj := strings.Index(ls[i:], clos)\n\tif j < 0 {\n\t\treturn \"\"\n\t}\n\tr := strings.TrimSpace(s[i : i+j])\n\tr = strings.ReplaceAll(r, \"&quot;\", \"\")\n\treturn strings.Trim(r, `\"'`)\n}\n\nfunc checkAPI(resp *resty.Response, result apiResult) error {\n\tif result.Result != \"\" && result.Result != \"ok\" {\n\t\tif result.Msg == \"\" {\n\t\t\tresult.Msg = \"unknown error\"\n\t\t}\n\t\treturn fmt.Errorf(\"%s: %s\", result.Result, result.Msg)\n\t}\n\tif resp != nil && resp.IsError() {\n\t\tif result.Msg != \"\" {\n\t\t\treturn fmt.Errorf(\"%s\", result.Msg)\n\t\t}\n\t\treturn fmt.Errorf(\"http error: %d\", resp.StatusCode())\n\t}\n\treturn nil\n}\n\nfunc (d *Wps) ensureCompanyID(ctx context.Context) error {\n\tif d.isPersonal() {\n\t\treturn nil\n\t}\n\tif d.companyID != \"\" {\n\t\treturn nil\n\t}\n\tvar resp workspaceResp\n\tr, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(endpoint + \"/3rd/plussvr/compose/v1/users/self/workspaces?fields=name&comp_status=active\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif r != nil && r.IsError() {\n\t\treturn fmt.Errorf(\"http error: %d\", r.StatusCode())\n\t}\n\tif len(resp.Companies) == 0 {\n\t\treturn fmt.Errorf(\"no company id\")\n\t}\n\td.companyID = strconv.FormatInt(resp.Companies[0].ID, 10)\n\treturn nil\n}\n\nfunc (d *Wps) getGroups(ctx context.Context) ([]Group, error) {\n\tif d.isPersonal() {\n\t\tvar resp personalGroupsResp\n\t\tr, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(d.driveURL(\"/api/v3/groups\"))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := checkAPI(r, resp.apiResult); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres := make([]Group, 0, len(resp.Groups))\n\t\tfor _, g := range resp.Groups {\n\t\t\tres = append(res, Group{GroupID: g.ID, Name: g.Name})\n\t\t}\n\t\treturn res, nil\n\t}\n\tif err := d.ensureCompanyID(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tvar resp groupsResp\n\turl := fmt.Sprintf(\"%s/3rd/plus/groups/v1/companies/%s/users/self/groups/private\", endpoint, d.companyID)\n\tr, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r != nil && r.IsError() {\n\t\treturn nil, fmt.Errorf(\"http error: %d\", r.StatusCode())\n\t}\n\treturn resp.Groups, nil\n}\n\nfunc (d *Wps) getFiles(ctx context.Context, groupID, parentID int64) ([]FileInfo, error) {\n\tvar resp filesResp\n\tvar files []FileInfo\n\tnext_offset := 0\n\tfor range 50 {\n\t\turl := fmt.Sprintf(\"%s/api/v5/groups/%d/files\", d.driveHost()+d.drivePrefix(), groupID)\n\t\tr, err := d.request(ctx).\n\t\t\tSetQueryParam(\"parentid\", strconv.FormatInt(parentID, 10)).\n\t\t\tSetQueryParam(\"offset\", fmt.Sprint(next_offset)).\n\t\t\tSetResult(&resp).\n\t\t\tSetError(&resp).\n\t\t\tGet(url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif r != nil && r.IsError() {\n\t\t\treturn nil, fmt.Errorf(\"http error: %d\", r.StatusCode())\n\t\t}\n\t\tfiles = append(files, resp.Files...)\n\t\tif resp.NextOffset == -1 {\n\t\t\tbreak\n\t\t}\n\t\tnext_offset = resp.NextOffset\n\t}\n\treturn files, nil\n}\n\nfunc parseTime(v int64) time.Time {\n\tif v <= 0 {\n\t\treturn time.Time{}\n\t}\n\treturn time.Unix(v, 0)\n}\n\nfunc joinPath(basePath, name string) string {\n\tif basePath == \"\" || basePath == \"/\" {\n\t\treturn \"/\" + name\n\t}\n\treturn strings.TrimRight(basePath, \"/\") + \"/\" + name\n}\n\nfunc normalizePath(path string) string {\n\tclean := strings.TrimSpace(path)\n\tif clean == \"\" || clean == \"/\" {\n\t\treturn \"/\"\n\t}\n\treturn \"/\" + strings.Trim(clean, \"/\")\n}\n\nfunc (d *Wps) resolveCacheStore() *resolveCacheStore {\n\tif d == nil {\n\t\treturn nil\n\t}\n\tif v, ok := resolveCaches.Load(d); ok {\n\t\tif s, ok := v.(*resolveCacheStore); ok {\n\t\t\treturn s\n\t\t}\n\t}\n\ts := &resolveCacheStore{m: make(map[string]resolveCacheEntry)}\n\tif v, loaded := resolveCaches.LoadOrStore(d, s); loaded {\n\t\tif s2, ok := v.(*resolveCacheStore); ok {\n\t\t\treturn s2\n\t\t}\n\t}\n\treturn s\n}\n\nfunc (d *Wps) getResolveCache(path string) (*resolvedNode, bool) {\n\ts := d.resolveCacheStore()\n\tif s == nil {\n\t\treturn nil, false\n\t}\n\ts.mu.RLock()\n\te, ok := s.m[path]\n\ts.mu.RUnlock()\n\tif !ok || e.node == nil {\n\t\treturn nil, false\n\t}\n\tif !e.expire.IsZero() && time.Now().After(e.expire) {\n\t\ts.mu.Lock()\n\t\tdelete(s.m, path)\n\t\ts.mu.Unlock()\n\t\treturn nil, false\n\t}\n\treturn e.node, true\n}\n\nfunc (d *Wps) setResolveCache(path string, node *resolvedNode) {\n\ts := d.resolveCacheStore()\n\tif s == nil || node == nil {\n\t\treturn\n\t}\n\ts.mu.Lock()\n\ts.m[path] = resolveCacheEntry{node: node, expire: time.Now().Add(10 * time.Minute)}\n\ts.mu.Unlock()\n}\n\nfunc (d *Wps) clearResolveCache() {\n\ts := d.resolveCacheStore()\n\tif s == nil {\n\t\treturn\n\t}\n\ts.mu.Lock()\n\tif len(s.m) != 0 {\n\t\ts.m = make(map[string]resolveCacheEntry)\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (d *Wps) resolvePath(ctx context.Context, path string) (*resolvedNode, error) {\n\tcacheKey := normalizePath(path)\n\tif n, ok := d.getResolveCache(cacheKey); ok {\n\t\treturn n, nil\n\t}\n\tclean := strings.TrimSpace(path)\n\tif clean == \"\" {\n\t\tclean = \"/\"\n\t}\n\tclean = strings.Trim(clean, \"/\")\n\tif clean == \"\" {\n\t\tn := &resolvedNode{kind: \"root\"}\n\t\td.setResolveCache(\"/\", n)\n\t\treturn n, nil\n\t}\n\tseg := strings.Split(clean, \"/\")\n\tgroups, err := d.getGroups(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar grp *Group\n\tfor i := range groups {\n\t\tif groups[i].Name == seg[0] {\n\t\t\tgrp = &groups[i]\n\t\t\tbreak\n\t\t}\n\t}\n\tif grp == nil {\n\t\treturn nil, fmt.Errorf(\"group not found\")\n\t}\n\tcur := \"/\" + seg[0]\n\tgn := &resolvedNode{kind: \"group\", group: *grp}\n\td.setResolveCache(cur, gn)\n\tif len(seg) == 1 {\n\t\treturn gn, nil\n\t}\n\tparentID := int64(0)\n\tvar lastNode *resolvedNode\n\tfor i := 1; i < len(seg); i++ {\n\t\tfiles, err := d.getFiles(ctx, grp.GroupID, parentID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar found *FileInfo\n\t\tfor j := range files {\n\t\t\tif files[j].Name == seg[i] {\n\t\t\t\tfound = &files[j]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif found == nil {\n\t\t\treturn nil, fmt.Errorf(\"path not found\")\n\t\t}\n\t\tif i < len(seg)-1 && found.Type != \"folder\" {\n\t\t\treturn nil, fmt.Errorf(\"path not found\")\n\t\t}\n\t\tfi := *found\n\t\tparentID = fi.ID\n\t\tcur = cur + \"/\" + seg[i]\n\t\tkind := \"file\"\n\t\tif fi.Type == \"folder\" {\n\t\t\tkind = \"folder\"\n\t\t}\n\t\tn := &resolvedNode{kind: kind, group: *grp, file: &fi}\n\t\td.setResolveCache(cur, n)\n\t\tlastNode = n\n\t}\n\tif lastNode == nil {\n\t\treturn nil, fmt.Errorf(\"path not found\")\n\t}\n\treturn lastNode, nil\n}\n\nfunc (d *Wps) fileToObj(basePath string, f FileInfo) *Obj {\n\tname := f.Name\n\tpath := joinPath(basePath, name)\n\tobj := &Obj{\n\t\tid:    path,\n\t\tname:  name,\n\t\tsize:  f.Size,\n\t\tctime: parseTime(f.Ctime),\n\t\tmtime: parseTime(f.Mtime),\n\t\tisDir: f.Type == \"folder\",\n\t\tpath:  path,\n\t}\n\tif !obj.isDir {\n\t\tobj.canDownload = d.canDownload(&f)\n\t}\n\treturn obj\n}\n\nfunc (d *Wps) doJSON(ctx context.Context, method, url string, body interface{}) error {\n\tvar result apiResult\n\treq := d.jsonRequest(ctx).SetBody(body).SetResult(&result).SetError(&result)\n\tvar (\n\t\tresp *resty.Response\n\t\terr  error\n\t)\n\tswitch method {\n\tcase http.MethodPost:\n\t\tresp, err = req.Post(url)\n\tcase http.MethodPut:\n\t\tresp, err = req.Put(url)\n\tdefault:\n\t\treturn errs.NotSupport\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn checkAPI(resp, result)\n}\n\nfunc (d *Wps) list(ctx context.Context, basePath string) ([]model.Obj, error) {\n\tif strings.TrimSpace(basePath) == \"\" {\n\t\tbasePath = \"/\"\n\t}\n\tnode, err := d.resolvePath(ctx, basePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif node.kind == \"root\" {\n\t\tgroups, err := d.getGroups(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres := make([]model.Obj, 0, len(groups))\n\t\tfor _, g := range groups {\n\t\t\tpath := joinPath(basePath, g.Name)\n\t\t\tobj := &Obj{\n\t\t\t\tid:    path,\n\t\t\t\tname:  g.Name,\n\t\t\t\tctime: parseTime(0),\n\t\t\t\tmtime: parseTime(0),\n\t\t\t\tisDir: true,\n\t\t\t\tpath:  path,\n\t\t\t}\n\t\t\tres = append(res, obj)\n\t\t\td.setResolveCache(normalizePath(path), &resolvedNode{kind: \"group\", group: g})\n\t\t}\n\t\td.setResolveCache(\"/\", &resolvedNode{kind: \"root\"})\n\t\treturn res, nil\n\t}\n\tif node.kind != \"group\" && node.kind != \"folder\" {\n\t\treturn nil, nil\n\t}\n\tparentID := int64(0)\n\tif node.file != nil && node.kind == \"folder\" {\n\t\tparentID = node.file.ID\n\t}\n\tfiles, err := d.getFiles(ctx, node.group.GroupID, parentID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]model.Obj, 0, len(files))\n\tfor _, f := range files {\n\t\tres = append(res, d.fileToObj(basePath, f))\n\t\tpath := normalizePath(joinPath(basePath, f.Name))\n\t\tfi := f\n\t\tkind := \"file\"\n\t\tif fi.Type == \"folder\" {\n\t\t\tkind = \"folder\"\n\t\t}\n\t\td.setResolveCache(path, &resolvedNode{kind: kind, group: node.group, file: &fi})\n\t}\n\treturn res, nil\n}\n\nfunc (d *Wps) link(ctx context.Context, path string) (*model.Link, error) {\n\tnode, err := d.resolvePath(ctx, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif node.kind != \"file\" || node.file == nil {\n\t\treturn nil, errs.NotSupport\n\t}\n\tif !d.canDownload(node.file) {\n\t\treturn nil, fmt.Errorf(\"no download permission\")\n\t}\n\turl := fmt.Sprintf(\"%s/api/v5/groups/%d/files/%d/download?support_checksums=sha1\", d.driveHost()+d.drivePrefix(), node.group.GroupID, node.file.ID)\n\tvar resp downloadResp\n\tr, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r != nil && r.IsError() {\n\t\treturn nil, fmt.Errorf(\"http error: %d\", r.StatusCode())\n\t}\n\tif resp.URL == \"\" {\n\t\treturn nil, fmt.Errorf(\"empty download url\")\n\t}\n\treturn &model.Link{URL: resp.URL, Header: http.Header{}}, nil\n}\n\nfunc (d *Wps) makeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif parentDir == nil {\n\t\treturn errs.NotSupport\n\t}\n\tnode, err := d.resolvePath(ctx, parentDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif node.kind != \"group\" && node.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\tparentID := int64(0)\n\tif node.file != nil && node.kind == \"folder\" {\n\t\tparentID = node.file.ID\n\t}\n\tbody := map[string]interface{}{\n\t\t\"groupid\":  node.group.GroupID,\n\t\t\"name\":     dirName,\n\t\t\"parentid\": parentID,\n\t}\n\tif err := d.doJSON(ctx, http.MethodPost, d.driveURL(\"/api/v5/files/folder\"), body); err != nil {\n\t\treturn err\n\t}\n\td.clearResolveCache()\n\treturn nil\n}\n\nfunc (d *Wps) move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif srcObj == nil || dstDir == nil {\n\t\treturn errs.NotSupport\n\t}\n\tnodeSrc, err := d.resolvePath(ctx, srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tnodeDst, err := d.resolvePath(ctx, dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif nodeSrc.kind != \"file\" && nodeSrc.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\tif nodeDst.kind != \"group\" && nodeDst.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\ttargetParentID := int64(0)\n\tif nodeDst.file != nil && nodeDst.kind == \"folder\" {\n\t\ttargetParentID = nodeDst.file.ID\n\t}\n\tbody := map[string]interface{}{\n\t\t\"fileids\":         []int64{nodeSrc.file.ID},\n\t\t\"target_groupid\":  nodeDst.group.GroupID,\n\t\t\"target_parentid\": targetParentID,\n\t}\n\turl := fmt.Sprintf(\"/api/v3/groups/%d/files/batch/move\", nodeSrc.group.GroupID)\n\tfor {\n\t\tvar res apiResult\n\t\tresp, err := d.jsonRequest(ctx).\n\t\t\tSetBody(body).\n\t\t\tSetResult(&res).\n\t\t\tSetError(&res).\n\t\t\tPost(d.driveURL(url))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif resp.StatusCode() == 403 && res.Result == \"fileTaskDuplicated\" {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := checkAPI(resp, res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tbreak\n\t}\n\td.clearResolveCache()\n\treturn nil\n}\n\nfunc (d *Wps) rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif srcObj == nil {\n\t\treturn errs.NotSupport\n\t}\n\tnode, err := d.resolvePath(ctx, srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif node.kind != \"file\" && node.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\turl := fmt.Sprintf(\"/api/v3/groups/%d/files/%d\", node.group.GroupID, node.file.ID)\n\tbody := map[string]string{\"fname\": newName}\n\tif err := d.doJSON(ctx, http.MethodPut, d.driveURL(url), body); err != nil {\n\t\treturn err\n\t}\n\td.clearResolveCache()\n\treturn nil\n}\n\nfunc (d *Wps) copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif srcObj == nil || dstDir == nil {\n\t\treturn errs.NotSupport\n\t}\n\tnodeSrc, err := d.resolvePath(ctx, srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tnodeDst, err := d.resolvePath(ctx, dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif nodeSrc.kind != \"file\" && nodeSrc.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\tif nodeDst.kind != \"group\" && nodeDst.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\ttargetParentID := int64(0)\n\tif nodeDst.file != nil && nodeDst.kind == \"folder\" {\n\t\ttargetParentID = nodeDst.file.ID\n\t}\n\tbody := map[string]interface{}{\n\t\t\"fileids\":               []int64{nodeSrc.file.ID},\n\t\t\"groupid\":               nodeSrc.group.GroupID,\n\t\t\"target_groupid\":        nodeDst.group.GroupID,\n\t\t\"target_parentid\":       targetParentID,\n\t\t\"duplicated_name_model\": 1,\n\t}\n\turl := fmt.Sprintf(\"/api/v3/groups/%d/files/batch/copy\", nodeSrc.group.GroupID)\n\tfor {\n\t\tvar res apiResult\n\t\tresp, err := d.jsonRequest(ctx).\n\t\t\tSetBody(body).\n\t\t\tSetResult(&res).\n\t\t\tSetError(&res).\n\t\t\tPost(d.driveURL(url))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif resp.StatusCode() == 403 && res.Result == \"fileTaskDuplicated\" {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := checkAPI(resp, res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tbreak\n\t}\n\td.clearResolveCache()\n\treturn nil\n}\n\nfunc (d *Wps) remove(ctx context.Context, obj model.Obj) error {\n\tif obj == nil {\n\t\treturn errs.NotSupport\n\t}\n\tnode, err := d.resolvePath(ctx, obj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif node.kind != \"file\" && node.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\n\tbody := map[string]interface{}{\n\t\t\"fileids\": []int64{node.file.ID},\n\t}\n\turl := fmt.Sprintf(\"/api/v3/groups/%d/files/batch/delete\", node.group.GroupID)\n\n\tfor {\n\t\tvar res apiResult\n\t\tresp, err := d.jsonRequest(ctx).\n\t\t\tSetBody(body).\n\t\t\tSetResult(&res).\n\t\t\tSetError(&res).\n\t\t\tPost(d.driveURL(url))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// 无法连续创建文件夹删除。如果一定要删除，每0.5s 尝试一次创建下一个删除请求，应当避免递归删除文件夹\n\t\tif resp.StatusCode() == 403 && res.Result == \"fileTaskDuplicated\" {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := checkAPI(resp, res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tbreak\n\t}\n\td.clearResolveCache()\n\treturn nil\n}\n\nfunc cacheAndHash(file model.FileStreamer, up driver.UpdateProgress) (model.File, int64, string, string, error) {\n\th1 := sha1.New()\n\th256 := sha256.New()\n\tsize := file.GetSize()\n\tvar counted int64\n\tws := []io.Writer{h1, h256}\n\tif size <= 0 {\n\t\tws = append(ws, countingWriter{n: &counted})\n\t}\n\tp := up\n\tf, err := file.CacheFullAndWriter(&p, io.MultiWriter(ws...))\n\tif err != nil {\n\t\treturn nil, 0, \"\", \"\", err\n\t}\n\tif size <= 0 {\n\t\tsize = counted\n\t}\n\treturn f, size, hex.EncodeToString(h1.Sum(nil)), hex.EncodeToString(h256.Sum(nil)), nil\n}\n\nfunc (d *Wps) createUpload(ctx context.Context, groupID, parentID int64, name string, size int64, sha1Hex, sha256Hex string) (*uploadCreateUpdateResp, error) {\n\tbody := map[string]string{\n\t\t\"group_id\":  strconv.FormatInt(groupID, 10),\n\t\t\"name\":      name,\n\t\t\"parent_id\": strconv.FormatInt(parentID, 10),\n\t\t\"sha1\":      sha1Hex,\n\t\t\"sha256\":    sha256Hex,\n\t\t\"size\":      strconv.FormatInt(size, 10),\n\t}\n\tvar resp uploadCreateUpdateResp\n\tr, err := d.jsonRequest(ctx).\n\t\tSetBody(body).\n\t\tSetResult(&resp).\n\t\tSetError(&resp).\n\t\tPut(d.driveURL(\"/api/v5/files/upload/create_update\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := checkAPI(r, resp.apiResult); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.URL == \"\" {\n\t\treturn nil, fmt.Errorf(\"empty upload url\")\n\t}\n\treturn &resp, nil\n}\n\nfunc normalizeETag(v string) string {\n\tv = strings.TrimSpace(v)\n\tif strings.HasPrefix(v, \"W/\") {\n\t\tv = strings.TrimSpace(strings.TrimPrefix(v, \"W/\"))\n\t}\n\treturn strings.Trim(v, `\"`)\n}\n\nfunc (d *Wps) commitUpload(ctx context.Context, etag, key string, groupID, parentID int64, name, sha1Hex string, size int64, store string) error {\n\tstore = strings.TrimSpace(store)\n\tif store == \"\" {\n\t\tstore = \"ks3\"\n\t}\n\tstoreKey := \"\"\n\tif key != \"\" {\n\t\tstoreKey = key\n\t}\n\tbody := map[string]interface{}{\n\t\t\"etag\":     etag,\n\t\t\"groupid\":  groupID,\n\t\t\"key\":      key,\n\t\t\"name\":     name,\n\t\t\"parentid\": parentID,\n\t\t\"sha1\":     sha1Hex,\n\t\t\"size\":     size,\n\t\t\"store\":    store,\n\t\t\"storekey\": storeKey,\n\t}\n\treturn d.doJSON(ctx, http.MethodPost, d.driveURL(\"/api/v5/files/file\"), body)\n}\n\nfunc (d *Wps) put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tif dstDir == nil || file == nil {\n\t\treturn errs.NotSupport\n\t}\n\tif up == nil {\n\t\tup = func(float64) {}\n\t}\n\tnode, err := d.resolvePath(ctx, dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif node.kind != \"group\" && node.kind != \"folder\" {\n\t\treturn errs.NotSupport\n\t}\n\tparentID := int64(0)\n\tif node.file != nil && node.kind == \"folder\" {\n\t\tparentID = node.file.ID\n\t}\n\tf, size, sha1Hex, sha256Hex, err := cacheAndHash(file, func(float64) {})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif c, ok := f.(io.Closer); ok {\n\t\tdefer c.Close()\n\t}\n\n\t// 在隐藏文件名前加_上传，这是WPS的限制，无法上传隐藏文件，也无法将任何文件重命名为隐藏文件，所有隐藏文件会被自动加上_ 上传\n\t// 甚至可以上传前缀是..的文件，但是单个点就是不行\n\trealName := file.GetName()\n\tuploadName := realName\n\tif strings.HasPrefix(realName, \".\") {\n\t\tuploadName = \"_\" + realName\n\t}\n\n\tinfo, err := d.createUpload(ctx, node.group.GroupID, parentID, uploadName, size, sha1Hex, sha256Hex)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := f.Seek(0, io.SeekStart); err != nil {\n\t\treturn err\n\t}\n\trf := driver.NewLimitedUploadFile(ctx, f)\n\tprog := driver.NewProgress(size, model.UpdateProgressWithRange(up, 0, 1))\n\n\tmethod := strings.ToUpper(strings.TrimSpace(info.Method))\n\tif method == \"\" {\n\t\tmethod = http.MethodPut\n\t}\n\n\tvar req *http.Request\n\tif method == http.MethodPost && len(info.Request.FormData) > 0 {\n\t\tif size == 0 {\n\t\t\tvar buf bytes.Buffer\n\t\t\tmw := multipart.NewWriter(&buf)\n\t\t\tfor k, v := range info.Request.FormData {\n\t\t\t\tif err := mw.WriteField(k, v); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tpart, err := mw.CreateFormFile(\"file\", uploadName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, err := io.Copy(part, io.TeeReader(rf, prog)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := mw.Close(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, method, info.URL, bytes.NewReader(buf.Bytes()))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor k, v := range info.Request.Headers {\n\t\t\t\treq.Header.Set(k, v)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\t\t\treq.ContentLength = int64(buf.Len())\n\t\t\treq.Header.Set(\"Content-Length\", strconv.FormatInt(req.ContentLength, 10))\n\t\t} else {\n\t\t\tpr, pw := io.Pipe()\n\t\t\tmw := multipart.NewWriter(pw)\n\t\t\treq, err = http.NewRequestWithContext(ctx, method, info.URL, pr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor k, v := range info.Request.Headers {\n\t\t\t\treq.Header.Set(k, v)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\t\t\tgo func() {\n\t\t\t\tfor k, v := range info.Request.FormData {\n\t\t\t\t\tif err := mw.WriteField(k, v); err != nil {\n\t\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tpart, err := mw.CreateFormFile(\"file\", uploadName)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif _, err := io.Copy(part, io.TeeReader(rf, prog)); err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := mw.Close(); err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tpw.Close()\n\t\t\t}()\n\t\t}\n\t} else {\n\t\tvar body = io.TeeReader(rf, prog)\n\t\tif size == 0 {\n\t\t\tbody = bytes.NewReader(nil)\n\t\t}\n\t\treq, err = http.NewRequestWithContext(ctx, method, info.URL, body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor k, v := range info.Request.Headers {\n\t\t\treq.Header.Set(k, v)\n\t\t}\n\t\treq.ContentLength = size\n\t\treq.Header.Set(\"Content-Length\", strconv.FormatInt(size, 10))\n\t}\n\n\tc := *base.RestyClient.GetClient()\n\tc.Timeout = 0\n\tresp, err := (&c).Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif !statusOK(resp.StatusCode, info.Response.ExpectCode) {\n\t\tio.Copy(io.Discard, resp.Body)\n\t\treturn fmt.Errorf(\"http error: %d\", resp.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tetag := normalizeETag(respArg(info.Response.ArgsETag, resp, body))\n\tif etag == \"\" {\n\t\tetag = normalizeETag(resp.Header.Get(\"ETag\"))\n\t}\n\n\tkey := strings.TrimSpace(respArg(info.Response.ArgsKey, resp, body))\n\tif key == \"\" {\n\t\tkey = strings.TrimSpace(resp.Header.Get(\"x-obs-save-key\"))\n\t}\n\n\tvar pr uploadPutResp\n\tsha1FromServer := \"\"\n\tif err := json.Unmarshal(body, &pr); err == nil {\n\t\tsha1FromServer = strings.TrimSpace(pr.NewFilename)\n\t\tif sha1FromServer == \"\" {\n\t\t\tsha1FromServer = strings.TrimSpace(pr.Sha1)\n\t\t}\n\t\tif etag == \"\" && pr.MD5 != \"\" {\n\t\t\tetag = strings.TrimSpace(pr.MD5)\n\t\t}\n\t}\n\n\tif sha1FromServer == \"\" {\n\t\tif v := extractXMLTag(string(body), \"ETag\"); v != \"\" {\n\t\t\tsha1FromServer = v\n\t\t\tif etag == \"\" {\n\t\t\t\tetag = v\n\t\t\t}\n\t\t}\n\t}\n\tif sha1FromServer == \"\" && key != \"\" && len(key) == 40 {\n\t\tsha1FromServer = key\n\t}\n\tif sha1FromServer == \"\" {\n\t\tsha1FromServer = sha1Hex\n\t}\n\n\tif etag == \"\" {\n\t\treturn fmt.Errorf(\"empty etag\")\n\t}\n\tif sha1FromServer == \"\" {\n\t\treturn fmt.Errorf(\"empty sha1\")\n\t}\n\n\tstore := strings.TrimSpace(info.Store)\n\tcommitKey := \"\"\n\tif strings.TrimSpace(info.Response.ArgsKey) != \"\" {\n\t\tcommitKey = key\n\t\tif commitKey == \"\" {\n\t\t\tcommitKey = sha1FromServer\n\t\t}\n\t}\n\n\tif err := d.commitUpload(ctx, etag, commitKey, node.group.GroupID, parentID, uploadName, sha1FromServer, size, store); err != nil {\n\t\treturn err\n\t}\n\n\tup(1)\n\treturn nil\n}\n\nfunc (d *Wps) spaces(ctx context.Context) (*spacesResp, error) {\n\turl := fmt.Sprintf(\"%s/api/v3/spaces\", d.driveHost()+d.drivePrefix())\n\tvar resp spacesResp\n\tr, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r != nil && r.IsError() {\n\t\treturn nil, fmt.Errorf(\"http error: %d\", r.StatusCode())\n\t}\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "drivers/yandex_disk/driver.go",
    "content": "package yandex_disk\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"path\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype YandexDisk struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string\n}\n\nfunc (d *YandexDisk) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *YandexDisk) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *YandexDisk) Init(ctx context.Context) error {\n\treturn d.refreshToken()\n}\n\nfunc (d *YandexDisk) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *YandexDisk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfiles, err := d.getFiles(dir.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\tobj := fileToObj(src)\n\t\tobj.Path = path.Join(dir.GetPath(), obj.Name)\n\t\treturn obj, nil\n\t})\n}\n\nfunc (d *YandexDisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar resp DownResp\n\t_, err := d.request(\"/download\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"path\", file.GetPath())\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlink := model.Link{\n\t\tURL: resp.Href,\n\t}\n\treturn &link, nil\n}\n\nfunc (d *YandexDisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t_, err := d.request(\"\", http.MethodPut, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"path\", path.Join(parentDir.GetPath(), dirName))\n\t}, nil)\n\treturn err\n}\n\nfunc (d *YandexDisk) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(\"/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"from\":      srcObj.GetPath(),\n\t\t\t\"path\":      path.Join(dstDir.GetPath(), srcObj.GetName()),\n\t\t\t\"overwrite\": \"true\",\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *YandexDisk) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t_, err := d.request(\"/move\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"from\":      srcObj.GetPath(),\n\t\t\t\"path\":      path.Join(path.Dir(srcObj.GetPath()), newName),\n\t\t\t\"overwrite\": \"true\",\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *YandexDisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t_, err := d.request(\"/copy\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"from\":      srcObj.GetPath(),\n\t\t\t\"path\":      path.Join(dstDir.GetPath(), srcObj.GetName()),\n\t\t\t\"overwrite\": \"true\",\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *YandexDisk) Remove(ctx context.Context, obj model.Obj) error {\n\t_, err := d.request(\"\", http.MethodDelete, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"path\", obj.GetPath())\n\t}, nil)\n\treturn err\n}\n\nfunc (d *YandexDisk) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tvar resp UploadResp\n\t_, err := d.request(\"/upload\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"path\":      path.Join(dstDir.GetPath(), s.GetName()),\n\t\t\t\"overwrite\": \"true\",\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\treq, err := http.NewRequestWithContext(ctx, resp.Method, resp.Href, reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Length\", strconv.FormatInt(s.GetSize(), 10))\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = res.Body.Close()\n\treturn err\n}\n\nvar _ driver.Driver = (*YandexDisk)(nil)\n"
  },
  {
    "path": "drivers/yandex_disk/meta.go",
    "content": "package yandex_disk\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Addition struct {\n\tRefreshToken   string `json:\"refresh_token\" required:\"true\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"name,path,created,modified,size\" default:\"name\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"asc,desc\" default:\"asc\"`\n\tdriver.RootPath\n\tUseOnlineAPI bool   `json:\"use_online_api\" default:\"true\"`\n\tAPIAddress   string `json:\"api_url_address\" default:\"https://api.oplist.org/yandexui/renewapi\"`\n\tClientID     string `json:\"client_id\"`\n\tClientSecret string `json:\"client_secret\"`\n}\n\nvar config = driver.Config{\n\tName:        \"YandexDisk\",\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &YandexDisk{}\n\t})\n}\n"
  },
  {
    "path": "drivers/yandex_disk/types.go",
    "content": "package yandex_disk\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype TokenErrResp struct {\n\tErrorDescription string `json:\"error_description\"`\n\tError            string `json:\"error\"`\n}\n\ntype ErrResp struct {\n\tMessage     string `json:\"message\"`\n\tDescription string `json:\"description\"`\n\tError       string `json:\"error\"`\n}\n\ntype File struct {\n\t//AntivirusStatus string `json:\"antivirus_status\"`\n\tSize int64 `json:\"size\"`\n\t//CommentIds      struct {\n\t//\tPrivateResource string `json:\"private_resource\"`\n\t//\tPublicResource  string `json:\"public_resource\"`\n\t//} `json:\"comment_ids\"`\n\tName string `json:\"name\"`\n\t//Exif struct {\n\t//\tDateTime time.Time `json:\"date_time\"`\n\t//} `json:\"exif\"`\n\t//Created    time.Time `json:\"created\"`\n\t//ResourceId string    `json:\"resource_id\"`\n\tModified time.Time `json:\"modified\"`\n\t//MimeType   string    `json:\"mime_type\"`\n\tFile string `json:\"file\"`\n\t//MediaType  string    `json:\"media_type\"`\n\tPreview string `json:\"preview\"`\n\tPath    string `json:\"path\"`\n\t//Sha256     string    `json:\"sha256\"`\n\tType string `json:\"type\"`\n\t//Md5        string    `json:\"md5\"`\n\t//Revision   int64     `json:\"revision\"`\n}\n\nfunc fileToObj(f File) *model.Object {\n\treturn &model.Object{\n\t\tName:     f.Name,\n\t\tSize:     f.Size,\n\t\tModified: f.Modified,\n\t\tIsFolder: f.Type == \"dir\",\n\t}\n}\n\ntype FilesResp struct {\n\tEmbedded struct {\n\t\tSort   string `json:\"sort\"`\n\t\tItems  []File `json:\"items\"`\n\t\tLimit  int    `json:\"limit\"`\n\t\tOffset int    `json:\"offset\"`\n\t\tPath   string `json:\"path\"`\n\t\tTotal  int    `json:\"total\"`\n\t} `json:\"_embedded\"`\n\tName string `json:\"name\"`\n\tExif struct {\n\t} `json:\"exif\"`\n\tResourceId string    `json:\"resource_id\"`\n\tCreated    time.Time `json:\"created\"`\n\tModified   time.Time `json:\"modified\"`\n\tPath       string    `json:\"path\"`\n\tCommentIds struct {\n\t} `json:\"comment_ids\"`\n\tType     string `json:\"type\"`\n\tRevision int64  `json:\"revision\"`\n}\n\ntype DownResp struct {\n\tHref      string `json:\"href\"`\n\tMethod    string `json:\"method\"`\n\tTemplated bool   `json:\"templated\"`\n}\n\ntype UploadResp struct {\n\tOperationId string `json:\"operation_id\"`\n\tHref        string `json:\"href\"`\n\tMethod      string `json:\"method\"`\n\tTemplated   bool   `json:\"templated\"`\n}\n"
  },
  {
    "path": "drivers/yandex_disk/util.go",
    "content": "package yandex_disk\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *YandexDisk) refreshToken() error {\n\t// 使用在线API刷新Token，无需ClientID和ClientSecret\n\tif d.UseOnlineAPI && len(d.APIAddress) > 0 {\n\t\tu := d.APIAddress\n\t\tvar resp struct {\n\t\t\tRefreshToken string `json:\"refresh_token\"`\n\t\t\tAccessToken  string `json:\"access_token\"`\n\t\t\tErrorMessage string `json:\"text\"`\n\t\t}\n\t\t_, err := base.RestyClient.R().\n\t\t\tSetResult(&resp).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"refresh_ui\": d.RefreshToken,\n\t\t\t\t\"server_use\": \"true\",\n\t\t\t\t\"driver_txt\": \"yandexui_go\",\n\t\t\t}).\n\t\t\tGet(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.RefreshToken == \"\" || resp.AccessToken == \"\" {\n\t\t\tif resp.ErrorMessage != \"\" {\n\t\t\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.ErrorMessage)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"empty token returned from official API , a wrong refresh token may have been used\")\n\t\t}\n\t\td.AccessToken = resp.AccessToken\n\t\td.RefreshToken = resp.RefreshToken\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\t// 使用本地客户端的情况下检查是否为空\n\tif d.ClientID == \"\" || d.ClientSecret == \"\" {\n\t\treturn fmt.Errorf(\"empty ClientID or ClientSecret\")\n\t}\n\t// 走原有的刷新逻辑\n\tu := \"https://oauth.yandex.com/token\"\n\tvar resp base.TokenResp\n\tvar e TokenErrResp\n\t_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{\n\t\t\"grant_type\":    \"refresh_token\",\n\t\t\"refresh_token\": d.RefreshToken,\n\t\t\"client_id\":     d.ClientID,\n\t\t\"client_secret\": d.ClientSecret,\n\t}).Post(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e.Error != \"\" {\n\t\treturn fmt.Errorf(\"%s : %s\", e.Error, e.ErrorDescription)\n\t}\n\td.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *YandexDisk) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tu := \"https://cloud-api.yandex.net/v1/disk/resources\" + pathname\n\treq := base.RestyClient.R()\n\treq.SetHeader(\"Authorization\", \"OAuth \"+d.AccessToken)\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\tvar e ErrResp\n\treq.SetError(&e)\n\tres, err := req.Execute(method, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//log.Debug(res.String())\n\tif e.Error != \"\" {\n\t\tif e.Error == \"UnauthorizedError\" {\n\t\t\terr = d.refreshToken()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.request(pathname, method, callback, resp)\n\t\t}\n\t\treturn nil, errors.New(e.Description)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *YandexDisk) getFiles(path string) ([]File, error) {\n\tlimit := 100\n\tpage := 1\n\tres := make([]File, 0)\n\tfor {\n\t\toffset := (page - 1) * limit\n\t\tquery := map[string]string{\n\t\t\t\"path\":   path,\n\t\t\t\"limit\":  strconv.Itoa(limit),\n\t\t\t\"offset\": strconv.Itoa(offset),\n\t\t}\n\t\tif d.OrderBy != \"\" {\n\t\t\tif d.OrderDirection == \"desc\" {\n\t\t\t\tquery[\"sort\"] = \"-\" + d.OrderBy\n\t\t\t} else {\n\t\t\t\tquery[\"sort\"] = d.OrderBy\n\t\t\t}\n\t\t}\n\t\tvar resp FilesResp\n\t\t_, err := d.request(\"\", http.MethodGet, func(req *resty.Request) {\n\t\t\treq.SetQueryParams(query)\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, resp.Embedded.Items...)\n\t\tif resp.Embedded.Total <= offset+limit {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "entrypoint.sh",
    "content": "#!/bin/sh\n\numask ${UMASK}\n\nif [ \"$1\" = \"version\" ]; then\n  ./openlist version\nelse\n  # Check file of /opt/openlist/data permissions for current user\n  # 检查当前用户是否有当前目录的写和执行权限\n  if [ -d ./data ]; then\n    if ! [ -w ./data ] || ! [ -x ./data ]; then\n  cat <<EOF\nError: Current user does not have write and/or execute permissions for the ./data directory: $(pwd)/data\nPlease visit https://doc.oplist.org/guide/installation/docker#for-version-after-v4-1-0 for more information.\n错误：当前用户没有 ./data 目录（$(pwd)/data）的写和/或执行权限。\n请访问 https://doc.oplist.org/guide/installation/docker#v4-1-0-%E4%BB%A5%E5%90%8E%E7%89%88%E6%9C%AC 获取更多信息。\nExiting...\nEOF\n      exit 1\n    fi\n  fi\n\n  # Define the target directory path for aria2 service\n  ARIA2_DIR=\"/opt/service/start/aria2\"\n  if [ \"$RUN_ARIA2\" = \"true\" ]; then\n    # If aria2 should run and target directory doesn't exist, copy it\n    if [ ! -d \"$ARIA2_DIR\" ]; then\n      mkdir -p \"$ARIA2_DIR\"\n      cp -r /opt/service/stop/aria2/* \"$ARIA2_DIR\" 2>/dev/null\n    fi\n    runsvdir /opt/service/start &\n  else\n    # If aria2 should NOT run and target directory exists, remove it\n    if [ -d \"$ARIA2_DIR\" ]; then\n      rm -rf \"$ARIA2_DIR\"\n    fi\n  fi\n  exec ./openlist server --no-prefix\nfi\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/OpenListTeam/OpenList/v4\n\ngo 1.24.0\n\ntoolchain go1.24.13\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2\n\tgithub.com/KarpelesLab/reflink v1.0.2\n\tgithub.com/KirCute/zip v1.0.1\n\tgithub.com/OpenListTeam/go-cache v0.1.0\n\tgithub.com/OpenListTeam/sftpd-openlist v1.0.1\n\tgithub.com/OpenListTeam/tache v0.2.2\n\tgithub.com/OpenListTeam/times v0.1.0\n\tgithub.com/OpenListTeam/wopan-sdk-go v0.1.5\n\tgithub.com/ProtonMail/go-crypto v1.3.0\n\tgithub.com/ProtonMail/gopenpgp/v2 v2.9.0\n\tgithub.com/SheltonZhu/115driver v1.2.3\n\tgithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible\n\tgithub.com/antchfx/htmlquery v1.3.5\n\tgithub.com/antchfx/xpath v1.3.5\n\tgithub.com/avast/retry-go v3.0.0+incompatible\n\tgithub.com/aws/aws-sdk-go v1.55.7\n\tgithub.com/blevesearch/bleve/v2 v2.5.2\n\tgithub.com/bmatcuk/doublestar/v4 v4.9.1\n\tgithub.com/caarlos0/env/v9 v9.0.0\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgithub.com/charmbracelet/bubbletea v1.3.6\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e\n\tgithub.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc\n\tgithub.com/coreos/go-oidc v2.3.0+incompatible\n\tgithub.com/deckarep/golang-set/v2 v2.8.0\n\tgithub.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/dlclark/regexp2 v1.11.5\n\tgithub.com/dustinxie/ecc v0.0.0-20210511000915-959544187564\n\tgithub.com/fclairamb/ftpserverlib v0.26.1-0.20250709223522-4a925d79caf6\n\tgithub.com/foxxorcat/mopan-sdk-go v0.1.6\n\tgithub.com/foxxorcat/weiyun-sdk-go v0.1.4\n\tgithub.com/gin-contrib/cors v1.7.6\n\tgithub.com/gin-gonic/gin v1.10.1\n\tgithub.com/go-resty/resty/v2 v2.16.5\n\tgithub.com/go-webauthn/webauthn v0.13.4\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/halalcloud/golang-sdk-lite v0.0.0-20251105081800-78cbb6786c38\n\tgithub.com/hekmon/transmissionrpc/v3 v3.0.0\n\tgithub.com/henrybear327/go-proton-api v1.0.0\n\tgithub.com/ipfs/go-ipfs-api v0.7.0\n\tgithub.com/itsHenry35/gofakes3 v0.0.8\n\tgithub.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3\n\tgithub.com/json-iterator/go v1.1.12\n\tgithub.com/kdomanski/iso9660 v0.4.0\n\tgithub.com/maruel/natural v1.1.1\n\tgithub.com/meilisearch/meilisearch-go v0.32.0\n\tgithub.com/mholt/archives v0.1.3\n\tgithub.com/natefinch/lumberjack v2.0.0+incompatible\n\tgithub.com/ncw/swift/v2 v2.0.4\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/pkg/sftp v1.13.9\n\tgithub.com/pquerna/otp v1.5.0\n\tgithub.com/quic-go/quic-go v0.54.1\n\tgithub.com/rclone/rclone v1.70.3\n\tgithub.com/shirou/gopsutil/v4 v4.25.5\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/afero v1.14.0\n\tgithub.com/spf13/cobra v1.9.1\n\tgithub.com/stretchr/testify v1.10.0\n\tgithub.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5\n\tgithub.com/tchap/go-patricia/v2 v2.3.3\n\tgithub.com/u2takey/ffmpeg-go v0.5.0\n\tgithub.com/upyun/go-sdk/v3 v3.0.4\n\tgithub.com/winfsp/cgofuse v1.6.0\n\tgithub.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3\n\tgolang.org/x/crypto v0.46.0\n\tgolang.org/x/image v0.29.0\n\tgolang.org/x/net v0.48.0\n\tgolang.org/x/oauth2 v0.34.0\n\tgolang.org/x/time v0.14.0\n\tgoogle.golang.org/appengine v1.6.8\n\tgopkg.in/ldap.v3 v3.1.0\n\tgorm.io/driver/mysql v1.5.7\n\tgorm.io/driver/postgres v1.5.9\n\tgorm.io/driver/sqlite v1.5.6\n\tgorm.io/gorm v1.25.11\n)\n\nrequire (\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect\n\tgithub.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect\n\tgithub.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e // indirect\n\tgithub.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect\n\tgithub.com/ProtonMail/go-srp v0.0.7 // indirect\n\tgithub.com/PuerkitoBio/goquery v1.10.3 // indirect\n\tgithub.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.3 // indirect\n\tgithub.com/bradenaw/juniper v0.15.3 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect\n\tgithub.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.5.0 // indirect\n\tgithub.com/cronokirby/saferith v0.33.0 // indirect\n\tgithub.com/ebitengine/purego v0.8.4 // indirect\n\tgithub.com/emersion/go-message v0.18.2 // indirect\n\tgithub.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff // indirect\n\tgithub.com/geoffgarside/ber v1.2.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/goidentity/v6 v6.0.1 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/lanrat/extsort v1.0.2 // indirect\n\tgithub.com/mikelolasagasti/xz v1.0.1 // indirect\n\tgithub.com/minio/minlz v1.0.0 // indirect\n\tgithub.com/minio/xxml v0.0.3 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/relvacode/iso8601 v1.6.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.uber.org/mock v0.5.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect\n\tgolang.org/x/mod v0.30.0 // indirect\n\tgopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect\n)\n\nrequire (\n\tgithub.com/OpenListTeam/115-sdk-go v0.2.3\n\tgithub.com/STARRY-S/zip v0.2.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/blevesearch/go-faiss v1.0.25 // indirect\n\tgithub.com/blevesearch/zapx/v16 v16.2.4 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.6.1\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/bytedance/sonic/loader v0.2.4 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.9.3 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/cloudflare/circl v1.6.1 // indirect\n\tgithub.com/cloudwego/base64x v0.1.5 // indirect\n\tgithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/fclairamb/go-log v0.6.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hekmon/cunits/v2 v2.1.0 // indirect\n\tgithub.com/ipfs/boxo v0.12.0 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.1 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/matoous/go-nanoid/v2 v2.1.0 // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.27\n\tgithub.com/nwaples/rardecode/v2 v2.1.1\n\tgithub.com/sorairolake/lzip-go v0.3.5 // indirect\n\tgithub.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 // indirect\n\tgithub.com/ulikunitz/xz v0.5.12 // indirect\n\tgithub.com/yuin/goldmark v1.7.13\n\tgo4.org v0.0.0-20260112195520-a5071408f32f\n\tresty.dev/v3 v3.0.0-beta.2 // indirect\n)\n\nrequire (\n\tgithub.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect\n\tgithub.com/OpenListTeam/gsync v0.1.0 // indirect\n\tgithub.com/abbot/go-http-auth v0.4.0 // indirect\n\tgithub.com/aead/ecdh v0.2.0 // indirect\n\tgithub.com/andreburgaud/crypt2go v1.8.0 // indirect\n\tgithub.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect\n\tgithub.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/benbjohnson/clock v1.3.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bits-and-blooms/bitset v1.22.0 // indirect\n\tgithub.com/blang/semver/v4 v4.0.0 // indirect\n\tgithub.com/blevesearch/bleve_index_api v1.2.8 // indirect\n\tgithub.com/blevesearch/geo v0.2.3 // indirect\n\tgithub.com/blevesearch/go-porterstemmer v1.0.3 // indirect\n\tgithub.com/blevesearch/gtreap v0.1.1 // indirect\n\tgithub.com/blevesearch/mmap-go v1.0.4 // indirect\n\tgithub.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect\n\tgithub.com/blevesearch/segment v0.9.1 // indirect\n\tgithub.com/blevesearch/snowballstem v0.9.0 // indirect\n\tgithub.com/blevesearch/upsidedown_store_api v1.0.2 // indirect\n\tgithub.com/blevesearch/vellum v1.1.0 // indirect\n\tgithub.com/blevesearch/zapx/v11 v11.4.2 // indirect\n\tgithub.com/blevesearch/zapx/v12 v12.4.2 // indirect\n\tgithub.com/blevesearch/zapx/v13 v13.4.2 // indirect\n\tgithub.com/blevesearch/zapx/v14 v14.4.2 // indirect\n\tgithub.com/blevesearch/zapx/v15 v15.4.2 // indirect\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/bytedance/sonic v1.13.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/coreos/go-semver v0.3.1 // indirect\n\tgithub.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.9 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.26.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.7.0 // indirect\n\tgithub.com/go-webauthn/x v0.1.23 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.2.3 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/go-tpm v0.9.5 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-version v1.6.0 // indirect\n\tgithub.com/henrybear327/Proton-API-Bridge v1.0.0\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/ipfs/go-cid v0.5.0\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect\n\tgithub.com/jackc/pgx/v5 v5.5.5 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.10 // indirect\n\tgithub.com/kr/fs v0.1.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/libp2p/go-buffer-pool v0.1.0 // indirect\n\tgithub.com/libp2p/go-flow-metrics v0.1.0 // indirect\n\tgithub.com/libp2p/go-libp2p v0.27.8 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.22 // indirect\n\tgithub.com/minio/sha256-simd v1.0.1 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/mr-tron/base58 v1.2.0 // indirect\n\tgithub.com/mschoch/smat v0.2.0 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/multiformats/go-base32 v0.1.0 // indirect\n\tgithub.com/multiformats/go-base36 v0.2.0 // indirect\n\tgithub.com/multiformats/go-multiaddr v0.9.0 // indirect\n\tgithub.com/multiformats/go-multibase v0.2.0 // indirect\n\tgithub.com/multiformats/go-multicodec v0.9.0 // indirect\n\tgithub.com/multiformats/go-multihash v0.2.3 // indirect\n\tgithub.com/multiformats/go-multistream v0.4.1 // indirect\n\tgithub.com/multiformats/go-varint v0.0.7 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/pquerna/cachecontrol v0.1.0 // indirect\n\tgithub.com/prometheus/client_golang v1.22.0 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.64.0 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgithub.com/rfjakob/eme v1.1.2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect\n\tgithub.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df // indirect\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgithub.com/spaolacci/murmur3 v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.6 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.15 // indirect\n\tgithub.com/tklauser/numcpus v0.10.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/u2takey/go-utils v0.3.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.etcd.io/bbolt v1.4.0 // indirect\n\tgolang.org/x/arch v0.18.0 // indirect\n\tgolang.org/x/sync v0.19.0\n\tgolang.org/x/sys v0.40.0\n\tgolang.org/x/term v0.38.0 // indirect\n\tgolang.org/x/text v0.32.0\n\tgolang.org/x/tools v0.39.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect\n\tgoogle.golang.org/grpc v1.78.0\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect\n\tgopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tlukechampine.com/blake3 v1.1.7 // indirect\n)\n\nreplace github.com/ProtonMail/go-proton-api => github.com/henrybear327/go-proton-api v1.0.0\n\nreplace github.com/cronokirby/saferith => github.com/Da3zKi7/saferith v0.33.0-fixed\n\n// replace github.com/OpenListTeam/115-sdk-go => ../../OpenListTeam/115-sdk-go\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.53.0 h1:MZQCQQaRwOrAcuKjiHWHrgKykt4fZyuwF2dtiG3fGW8=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=\ncloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=\ncloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=\ngithub.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Da3zKi7/saferith v0.33.0-fixed h1:fnIWTk7EP9mZAICf7aQjeoAwpfrlCrkOvqmi6CbWdTk=\ngithub.com/Da3zKi7/saferith v0.33.0-fixed/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=\ngithub.com/KarpelesLab/reflink v1.0.2 h1:hQ1aM3TmjU2kTNUx5p/HaobDoADYk+a6AuEinG4Cv88=\ngithub.com/KarpelesLab/reflink v1.0.2/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok=\ngithub.com/KirCute/zip v1.0.1 h1:L/tVZglOiDVKDi9Ud+fN49htgKdQ3Z0H80iX8OZk13c=\ngithub.com/KirCute/zip v1.0.1/go.mod h1:xhF7dCB+Bjvy+5a56lenYCKBsH+gxDNPZSy5Cp+nlXk=\ngithub.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=\ngithub.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=\ngithub.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=\ngithub.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=\ngithub.com/OpenListTeam/115-sdk-go v0.2.3 h1:nDNz0GxgliW+nT2Ds486k/rp/GgJj7Ngznc98ZBUwZo=\ngithub.com/OpenListTeam/115-sdk-go v0.2.3/go.mod h1:cfvitk2lwe6036iNi2h+iNxwxWDifKZsSvNtrur5BqU=\ngithub.com/OpenListTeam/go-cache v0.1.0 h1:eV2+FCP+rt+E4OCJqLUW7wGccWZNJMV0NNkh+uChbAI=\ngithub.com/OpenListTeam/go-cache v0.1.0/go.mod h1:AHWjKhNK3LE4rorVdKyEALDHoeMnP8SjiNyfVlB+Pz4=\ngithub.com/OpenListTeam/gsync v0.1.0 h1:ywzGybOvA3lW8K1BUjKZ2IUlT2FSlzPO4DOazfYXjcs=\ngithub.com/OpenListTeam/gsync v0.1.0/go.mod h1:h/Rvv9aX/6CdW/7B8di3xK3xNV8dUg45Fehrd/ksZ9s=\ngithub.com/OpenListTeam/sftpd-openlist v1.0.1 h1:j4S3iPFOpnXCUKRPS7uCT4mF2VCl34GyqvH6lqwnkUU=\ngithub.com/OpenListTeam/sftpd-openlist v1.0.1/go.mod h1:uO/wKnbvbdq3rBLmClMTZXuCnw7XW4wlAq4dZe91a40=\ngithub.com/OpenListTeam/tache v0.2.2 h1:CWFn6sr1AIYaEjC8ONdKs+LrxHyuErheenAjEqRhh4k=\ngithub.com/OpenListTeam/tache v0.2.2/go.mod h1:qmnZ/VpY2DUlmjg3UoDeNFy/LRqrw0biN3hYEEGc/+A=\ngithub.com/OpenListTeam/times v0.1.0 h1:qknxw+qj5CYKgXAwydA102UEpPcpU8TYNGRmwRyPYpg=\ngithub.com/OpenListTeam/times v0.1.0/go.mod h1:Jx7qen5NCYzKk2w14YuvU48YYMcPa1P9a+EJePC15Pc=\ngithub.com/OpenListTeam/wopan-sdk-go v0.1.5 h1:iKKcVzIqBgtGDbn0QbdWrCazSGxXFmYFyrnFBG+U8dI=\ngithub.com/OpenListTeam/wopan-sdk-go v0.1.5/go.mod h1:otynv0CgSNUClPpUgZ44qCZGcMRe0dc83Pkk65xAunI=\ngithub.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=\ngithub.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=\ngithub.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=\ngithub.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e h1:lCsqUUACrcMC83lg5rTo9Y0PnPItE61JSfvMyIcANwk=\ngithub.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=\ngithub.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=\ngithub.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=\ngithub.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=\ngithub.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=\ngithub.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=\ngithub.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=\ngithub.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE=\ngithub.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s=\ngithub.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=\ngithub.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=\ngithub.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=\ngithub.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=\ngithub.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=\ngithub.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=\ngithub.com/SheltonZhu/115driver v1.2.3 h1:94XMP/ey7VXIlpoBLIJHEoXu7N8YsELZlXVbxWcDDvk=\ngithub.com/SheltonZhu/115driver v1.2.3/go.mod h1:Zk7Qz7SYO1QU0SJIne6DnUD2k36S3wx/KbsQpxcfY/Y=\ngithub.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=\ngithub.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=\ngithub.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=\ngithub.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=\ngithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=\ngithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=\ngithub.com/andreburgaud/crypt2go v1.8.0 h1:J73vGTb1P6XL69SSuumbKs0DWn3ulbl9L92ZXBjw6pc=\ngithub.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAPJAF5fKOLB9SXg=\ngithub.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=\ngithub.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=\ngithub.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=\ngithub.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=\ngithub.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=\ngithub.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=\ngithub.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=\ngithub.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ=\ngithub.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=\ngithub.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=\ngithub.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=\ngithub.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=\ngithub.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=\ngithub.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.49 h1:7gss+6H2mrrFtBrkokJRR2TzQD9qkpGA4N6BvIP/pCM=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.49/go.mod h1:30PBx0ENoUCJm2AxzgCue8j7KEjb9ci4enxy6CCOjbE=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.72.3 h1:WZOmJfCDV+4tYacLxpiojoAdT5sxTfB3nTqQNtZu+J4=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.72.3/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=\ngithub.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=\ngithub.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=\ngithub.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=\ngithub.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=\ngithub.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=\ngithub.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=\ngithub.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=\ngithub.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8=\ngithub.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo=\ngithub.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=\ngithub.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=\ngithub.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg=\ngithub.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=\ngithub.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=\ngithub.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=\ngithub.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=\ngithub.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=\ngithub.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=\ngithub.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=\ngithub.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=\ngithub.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=\ngithub.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s=\ngithub.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=\ngithub.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=\ngithub.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=\ngithub.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=\ngithub.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=\ngithub.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=\ngithub.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=\ngithub.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=\ngithub.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=\ngithub.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=\ngithub.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=\ngithub.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=\ngithub.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=\ngithub.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=\ngithub.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=\ngithub.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=\ngithub.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=\ngithub.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=\ngithub.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=\ngithub.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww=\ngithub.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs=\ngithub.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=\ngithub.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=\ngithub.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=\ngithub.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=\ngithub.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=\ngithub.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=\ngithub.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/bradenaw/juniper v0.15.3 h1:RHIAMEDTpvmzV1wg1jMAHGOoI2oJUSPx3lxRldXnFGo=\ngithub.com/bradenaw/juniper v0.15.3/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8=\ngithub.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=\ngithub.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=\ngithub.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=\ngithub.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=\ngithub.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=\ngithub.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=\ngithub.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=\ngithub.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e h1:GLC8iDDcbt1H8+RkNao2nRGjyNTIo81e1rAJT9/uWYA=\ngithub.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e/go.mod h1:ln9Whp+wVY/FTbn2SK0ag+SKD2fC0yQCF/Lqowc1LmU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=\ngithub.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=\ngithub.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=\ngithub.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc h1:t8YjNUCt1DimB4HCIXBztwWMhgxr5yG5/YaRl9Afdfg=\ngithub.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc/go.mod h1:CgWpFCFWzzEA5hVkhAc6DZZzGd3czx+BblvOzjmg6KA=\ngithub.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs=\ngithub.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E=\ngithub.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=\ngithub.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0=\ngithub.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=\ngithub.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=\ngithub.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=\ngithub.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=\ngithub.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=\ngithub.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=\ngithub.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=\ngithub.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=\ngithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=\ngithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=\ngithub.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=\ngithub.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=\ngithub.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=\ngithub.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=\ngithub.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=\ngithub.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=\ngithub.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=\ngithub.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/fclairamb/ftpserverlib v0.26.1-0.20250709223522-4a925d79caf6 h1:q1b+gv6AG2TDPN+f0QAkbRrAvJ3ZosnwRLTKNxSXlaA=\ngithub.com/fclairamb/ftpserverlib v0.26.1-0.20250709223522-4a925d79caf6/go.mod h1:MAsn6OKL24MLbGdCjt1t44XMGgX3sFqukYTKmTUOci8=\ngithub.com/fclairamb/go-log v0.6.0 h1:1V7BJ75P2PvanLHRyGBBFjncB6d4AgEmu+BPWKbMkaU=\ngithub.com/fclairamb/go-log v0.6.0/go.mod h1:cyXxOw4aJwO6lrZb8GRELSw+sxO6wwkLJdsjY5xYCWA=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=\ngithub.com/foxxorcat/mopan-sdk-go v0.1.6/go.mod h1:UaY6D88yBXWGrcu/PcyLWyL4lzrk5pSxSABPHftOvxs=\ngithub.com/foxxorcat/weiyun-sdk-go v0.1.4 h1:X2tFvdqikkJ7awCBbMH7XXk7+uQoJlQksJz9CUU6ZgA=\ngithub.com/foxxorcat/weiyun-sdk-go v0.1.4/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=\ngithub.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=\ngithub.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=\ngithub.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=\ngithub.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=\ngithub.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=\ngithub.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=\ngithub.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 h1:JnrjqG5iR07/8k7NqrLNilRsl3s1EPRQEGvbPyOce68=\ngithub.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348/go.mod h1:Czxo/d1g948LtrALAZdL04TL/HnkopquAjxYUuI02bo=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=\ngithub.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=\ngithub.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=\ngithub.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=\ngithub.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=\ngithub.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=\ngithub.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=\ngithub.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=\ngithub.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=\ngithub.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=\ngithub.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=\ngithub.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=\ngithub.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=\ngithub.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/halalcloud/golang-sdk-lite v0.0.0-20251105081800-78cbb6786c38 h1:lsK2GVgI2Ox0NkRpQnN09GBOH7jtsjFK5tcIgxXlLr0=\ngithub.com/halalcloud/golang-sdk-lite v0.0.0-20251105081800-78cbb6786c38/go.mod h1:8x1h4rm3s8xMcTyJrq848sQ6BJnKzl57mDY4CNshdPM=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=\ngithub.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI0=\ngithub.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M=\ngithub.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ=\ngithub.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg=\ngithub.com/henrybear327/Proton-API-Bridge v1.0.0 h1:gjKAaWfKu++77WsZTHg6FUyPC5W0LTKWQciUm8PMZb0=\ngithub.com/henrybear327/Proton-API-Bridge v1.0.0/go.mod h1:gunH16hf6U74W2b9CGDaWRadiLICsoJ6KRkSt53zLts=\ngithub.com/henrybear327/go-proton-api v1.0.0 h1:zYi/IbjLwFAW7ltCeqXneUGJey0TN//Xo851a/BgLXw=\ngithub.com/henrybear327/go-proton-api v1.0.0/go.mod h1:w63MZuzufKcIZ93pwRgiOtxMXYafI8H74D77AxytOBc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/ipfs/boxo v0.12.0 h1:AXHg/1ONZdRQHQLgG5JHsSC3XoE4DjCAMgK+asZvUcQ=\ngithub.com/ipfs/boxo v0.12.0/go.mod h1:xAnfiU6PtxWCnRqu7dcXQ10bB5/kvI1kXRotuGqGBhg=\ngithub.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=\ngithub.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=\ngithub.com/ipfs/go-ipfs-api v0.7.0 h1:CMBNCUl0b45coC+lQCXEVpMhwoqjiaCwUIrM+coYW2Q=\ngithub.com/ipfs/go-ipfs-api v0.7.0/go.mod h1:AIxsTNB0+ZhkqIfTZpdZ0VR/cpX5zrXjATa3prSay3g=\ngithub.com/itsHenry35/gofakes3 v0.0.8 h1:1AgOl04IgoUV5r/WSK7ycnvwfpgharYLfVTmnzk5miw=\ngithub.com/itsHenry35/gofakes3 v0.0.8/go.mod h1:gQwOJ7LoH5QSpCVmjzC6oKp+MS71utLS7GHtonsvD0c=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=\ngithub.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=\ngithub.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=\ngithub.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3 h1:ZxO6Qr2GOXPdcW80Mcn3nemvilMPvpWqxrNfK2ZnNNs=\ngithub.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3/go.mod h1:dvLUr/8Fs9a2OBrEnCC5duphbkz/k/mSy5OkXg3PAgI=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg=\ngithub.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c=\ngithub.com/kdomanski/iso9660 v0.4.0 h1:BPKKdcINz3m0MdjIMwS0wx1nofsOjxOq8TOr45WGHFg=\ngithub.com/kdomanski/iso9660 v0.4.0/go.mod h1:OxUSupHsO9ceI8lBLPJKWBTphLemjrCQY8LPXM7qSzU=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lanrat/extsort v1.0.2 h1:p3MLVpQEPwEGPzeLBb+1eSErzRl6Bgjgr+qnIs2RxrU=\ngithub.com/lanrat/extsort v1.0.2/go.mod h1:ivzsdLm8Tv+88qbdpMElV6Z15StlzPUtZSKsGb51hnQ=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=\ngithub.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=\ngithub.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM=\ngithub.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro=\ngithub.com/libp2p/go-libp2p v0.27.8 h1:IX5x/4yKwyPQeVS2AXHZ3J4YATM9oHBGH1gBc23jBAI=\ngithub.com/libp2p/go-libp2p v0.27.8/go.mod h1:eCFFtd0s5i/EVKR7+5Ki8bM7qwkNW3TPTTSSW9sz8NE=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=\ngithub.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=\ngithub.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=\ngithub.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=\ngithub.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/meilisearch/meilisearch-go v0.32.0 h1:cWcycpONSH3VLTZ5npUl1O5aXPkNM0vUx6bywnYqGbE=\ngithub.com/meilisearch/meilisearch-go v0.32.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g=\ngithub.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=\ngithub.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=\ngithub.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=\ngithub.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=\ngithub.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=\ngithub.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=\ngithub.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=\ngithub.com/minio/xxml v0.0.3 h1:ZIpPQpfyG5uZQnqqC0LZuWtPk/WT8G/qkxvO6jb7zMU=\ngithub.com/minio/xxml v0.0.3/go.mod h1:wcXErosl6IezQIMEWSK/LYC2VS7LJ1dAkgvuyIN3aH4=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=\ngithub.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=\ngithub.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=\ngithub.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=\ngithub.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=\ngithub.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=\ngithub.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=\ngithub.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sggzwC/NcqbDQ=\ngithub.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0=\ngithub.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=\ngithub.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=\ngithub.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=\ngithub.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=\ngithub.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=\ngithub.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=\ngithub.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo=\ngithub.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q=\ngithub.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=\ngithub.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=\ngithub.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=\ngithub.com/ncw/swift/v2 v2.0.4 h1:hHWVFxn5/YaTWAASmn4qyq2p6OyP/Hm3vMLzkjEqR7w=\ngithub.com/ncw/swift/v2 v2.0.4/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=\ngithub.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=\ngithub.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=\ngithub.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=\ngithub.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=\ngithub.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=\ngithub.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=\ngithub.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=\ngithub.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=\ngithub.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=\ngithub.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=\ngithub.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=\ngithub.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=\ngithub.com/rclone/rclone v1.70.3 h1:rg/WNh4DmSVZyKP2tHZ4lAaWEyMi7h/F0r7smOMA3IE=\ngithub.com/rclone/rclone v1.70.3/go.mod h1:nLyN+hpxAsQn9Rgt5kM774lcRDad82x/KqQeBZ83cMo=\ngithub.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU=\ngithub.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=\ngithub.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4=\ngithub.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=\ngithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=\ngithub.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=\ngithub.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=\ngithub.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM=\ngithub.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=\ngithub.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=\ngithub.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=\ngithub.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=\ngithub.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=\ngithub.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=\ngithub.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=\ngithub.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=\ngithub.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=\ngithub.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=\ngithub.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=\ngithub.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=\ngithub.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5 h1:Sa+sR8aaAMFwxhXWENEnE6ZpqhZ9d7u1RT2722Rw6hc=\ngithub.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5/go.mod h1:UdZiFUFu6e2WjjtjxivwXWcwc1N/8zgbkBR9QNucUOY=\ngithub.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=\ngithub.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=\ngithub.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=\ngithub.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=\ngithub.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=\ngithub.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=\ngithub.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=\ngithub.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=\ngithub.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=\ngithub.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=\ngithub.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=\ngithub.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=\ngithub.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=\ngithub.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/unknwon/goconfig v1.0.0 h1:rS7O+CmUdli1T+oDm7fYj1MwqNWtEJfNj+FqcUHML8U=\ngithub.com/unknwon/goconfig v1.0.0/go.mod h1:qu2ZQ/wcC/if2u32263HTVC39PeOQRSmidQk3DuDFQ8=\ngithub.com/upyun/go-sdk/v3 v3.0.4 h1:2DCJa/Yi7/3ZybT9UCPATSzvU3wpPPxhXinNlb1Hi8Q=\ngithub.com/upyun/go-sdk/v3 v3.0.4/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E=\ngithub.com/winfsp/cgofuse v1.6.0 h1:re3W+HTd0hj4fISPBqfsrwyvPFpzqhDu8doJ9nOPDB0=\ngithub.com/winfsp/cgofuse v1.6.0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=\ngithub.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngithub.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3 h1:PSRwrE5QBufPnOjdgIkRs5KBV1Avq3SY8oksj2Z+k3o=\ngithub.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3/go.mod h1:CKriYB8bkNgSbYUQF1khSpejKb5IsV6cR7MdaAR7Fc0=\ngo.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=\ngo.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=\ngo.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=\ngo.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=\ngo.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=\ngo.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=\ngo.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=\ngo.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=\ngo.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=\ngo.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=\ngo.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=\ngo.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=\ngo.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngo4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=\ngo4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=\ngocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=\ngolang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=\ngolang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=\ngolang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=\ngolang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=\ngolang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=\ngolang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=\ngolang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=\ngolang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=\ngolang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=\ngolang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=\ngolang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=\ngolang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=\ngolang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=\ngolang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=\ngolang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=\ngolang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=\ngolang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=\ngolang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngolang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=\ngolang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=\ngolang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=\ngolang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=\ngolang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0=\ngoogle.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=\ngoogle.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=\ngoogle.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=\ngoogle.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=\ngoogle.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=\ngopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=\ngopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=\ngopkg.in/ldap.v3 v3.1.0 h1:DIDWEjI7vQWREh0S8X5/NFPCZ3MCVd55LmXKPW4XLGE=\ngopkg.in/ldap.v3 v3.1.0/go.mod h1:dQjCc0R0kfyFjIlWNMH1DORwUASZyDxo2Ry1B51dXaQ=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=\ngorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=\ngorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=\ngorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=\ngorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=\ngorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=\ngorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=\ngorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=\ngorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nlukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=\nlukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nresty.dev/v3 v3.0.0-beta.2 h1:xu4mGAdbCLuc3kbk7eddWfWm4JfhwDtdapwss5nCjnQ=\nresty.dev/v3 v3.0.0-beta.2/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "internal/archive/all.go",
    "content": "package archive\n\nimport (\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive/archives\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive/iso9660\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive/rardecode\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive/sevenzip\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/archive/zip\"\n)\n"
  },
  {
    "path": "internal/archive/archives/archives.go",
    "content": "package archives\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Archives struct {\n}\n\nfunc (Archives) AcceptedExtensions() []string {\n\treturn []string{\n\t\t\".br\", \".bz2\", \".gz\", \".lz4\", \".lz\", \".mz\", \".sz\", \".s2\", \".xz\", \".zz\", \".zst\", \".tar\",\n\t\t\".tgz\", \".tlz4\", \".tlz\", \".tbz2\", \".txz\", \".tzst\",\n\t}\n}\n\nfunc (Archives) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{}\n}\n\nfunc (Archives) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tfsys, err := getFs(ss[0], args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := fsys.ReadDir(\".\")\n\tif err != nil {\n\t\treturn nil, filterPassword(err)\n\t}\n\n\ttree := make([]model.ObjTree, 0, len(files))\n\tfor _, file := range files {\n\t\tinfo, err := file.Info()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\ttree = append(tree, &model.ObjectTree{Object: *toModelObj(info)})\n\t}\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   \"\",\n\t\tEncrypted: false,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (Archives) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tfsys, err := getFs(ss[0], args.ArchiveArgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\tif innerPath == \"\" {\n\t\tinnerPath = \".\"\n\t}\n\tobj, err := fsys.ReadDir(innerPath)\n\tif err != nil {\n\t\treturn nil, filterPassword(err)\n\t}\n\treturn utils.SliceConvert(obj, func(src os.DirEntry) (model.Obj, error) {\n\t\tinfo, err := src.Info()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn toModelObj(info), nil\n\t})\n}\n\nfunc (Archives) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\tfsys, err := getFs(ss[0], args.ArchiveArgs)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tfile, err := fsys.Open(strings.TrimPrefix(args.InnerPath, \"/\"))\n\tif err != nil {\n\t\treturn nil, 0, filterPassword(err)\n\t}\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn nil, 0, filterPassword(err)\n\t}\n\treturn file, stat.Size(), nil\n}\n\nfunc (Archives) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\tfsys, err := getFs(ss[0], args.ArchiveArgs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tisDir := false\n\tpath := strings.TrimPrefix(args.InnerPath, \"/\")\n\tif path == \"\" {\n\t\tisDir = true\n\t\tpath = \".\"\n\t} else {\n\t\tstat, err := fsys.Stat(path)\n\t\tif err != nil {\n\t\t\treturn filterPassword(err)\n\t\t}\n\t\tif stat.IsDir() {\n\t\t\tisDir = true\n\t\t\toutputPath = filepath.Join(outputPath, stat.Name())\n\t\t\terr = os.Mkdir(outputPath, 0700)\n\t\t\tif err != nil {\n\t\t\t\treturn filterPassword(err)\n\t\t\t}\n\t\t}\n\t}\n\tif isDir {\n\t\terr = fs.WalkDir(fsys, path, func(p string, d fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trelPath := strings.TrimPrefix(p, path+\"/\")\n\t\t\tdstPath := filepath.Join(outputPath, relPath)\n\t\t\tif !strings.HasPrefix(dstPath, outputPath+string(os.PathSeparator)) {\n\t\t\t\treturn fmt.Errorf(\"illegal file path: %s\", relPath)\n\t\t\t}\n\t\t\tif d.IsDir() {\n\t\t\t\terr = os.MkdirAll(dstPath, 0700)\n\t\t\t} else {\n\t\t\t\tdir := filepath.Dir(dstPath)\n\t\t\t\terr = decompress(fsys, p, dir, func(_ float64) {})\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t} else {\n\t\terr = decompress(fsys, path, outputPath, up)\n\t}\n\treturn filterPassword(err)\n}\n\nvar _ tool.Tool = (*Archives)(nil)\n\nfunc init() {\n\ttool.RegisterTool(Archives{})\n}\n"
  },
  {
    "path": "internal/archive/archives/utils.go",
    "content": "package archives\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\tfs2 \"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/mholt/archives\"\n)\n\nfunc getFs(ss *stream.SeekableStream, args model.ArchiveArgs) (*archives.ArchiveFS, error) {\n\treader, err := stream.NewReadAtSeeker(ss, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r, ok := reader.(*stream.RangeReadReadAtSeeker); ok {\n\t\tr.InitHeadCache()\n\t}\n\tformat, _, err := archives.Identify(ss.Ctx, ss.GetName(), reader)\n\tif err != nil {\n\t\treturn nil, errs.UnknownArchiveFormat\n\t}\n\textractor, ok := format.(archives.Extractor)\n\tif !ok {\n\t\treturn nil, errs.UnknownArchiveFormat\n\t}\n\tswitch f := format.(type) {\n\tcase archives.SevenZip:\n\t\tf.Password = args.Password\n\tcase archives.Rar:\n\t\tf.Password = args.Password\n\t}\n\treturn &archives.ArchiveFS{\n\t\tStream:  io.NewSectionReader(reader, 0, ss.GetSize()),\n\t\tFormat:  extractor,\n\t\tContext: ss.Ctx,\n\t}, nil\n}\n\nfunc toModelObj(file os.FileInfo) *model.Object {\n\treturn &model.Object{\n\t\tName:     file.Name(),\n\t\tSize:     file.Size(),\n\t\tModified: file.ModTime(),\n\t\tIsFolder: file.IsDir(),\n\t}\n}\n\nfunc filterPassword(err error) error {\n\tif err != nil && strings.Contains(err.Error(), \"password\") {\n\t\treturn errs.WrongArchivePassword\n\t}\n\treturn err\n}\n\nfunc decompress(fsys fs2.FS, filePath, targetPath string, up model.UpdateProgress) error {\n\trc, err := fsys.Open(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rc.Close()\n\tstat, err := rc.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdestPath := filepath.Join(targetPath, stat.Name())\n\tif !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {\n\t\treturn fmt.Errorf(\"illegal file path: %s\", stat.Name())\n\t}\n\tf, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\t_, err = utils.CopyWithBuffer(f, &stream.ReaderUpdatingProgress{\n\t\tReader: &stream.SimpleReaderWithSize{\n\t\t\tReader: rc,\n\t\t\tSize:   stat.Size(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "internal/archive/iso9660/iso9660.go",
    "content": "package iso9660\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/kdomanski/iso9660\"\n)\n\ntype ISO9660 struct {\n}\n\nfunc (ISO9660) AcceptedExtensions() []string {\n\treturn []string{\".iso\"}\n}\n\nfunc (ISO9660) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{}\n}\n\nfunc (ISO9660) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   \"\",\n\t\tEncrypted: false,\n\t}, nil\n}\n\nfunc (ISO9660) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\timg, err := getImage(ss[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdir, err := getObj(img, args.InnerPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !dir.IsDir() {\n\t\treturn nil, errs.NotFolder\n\t}\n\tchildren, err := dir.GetChildren()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tret := make([]model.Obj, 0, len(children))\n\tfor _, child := range children {\n\t\tret = append(ret, toModelObj(child))\n\t}\n\treturn ret, nil\n}\n\nfunc (ISO9660) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\timg, err := getImage(ss[0])\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tobj, err := getObj(img, args.InnerPath)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tif obj.IsDir() {\n\t\treturn nil, 0, errs.NotFile\n\t}\n\treturn io.NopCloser(obj.Reader()), obj.Size(), nil\n}\n\nfunc (ISO9660) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\timg, err := getImage(ss[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\tobj, err := getObj(img, args.InnerPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif obj.IsDir() {\n\t\tif args.InnerPath != \"/\" {\n\t\t\trootpath := outputPath\n\t\t\toutputPath = filepath.Join(outputPath, obj.Name())\n\t\t\tif !strings.HasPrefix(outputPath, rootpath+string(os.PathSeparator)) {\n\t\t\t\treturn fmt.Errorf(\"illegal file path: %s\", obj.Name())\n\t\t\t}\n\t\t\tif err = os.MkdirAll(outputPath, 0700); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tvar children []*iso9660.File\n\t\tif children, err = obj.GetChildren(); err == nil {\n\t\t\terr = decompressAll(children, outputPath)\n\t\t}\n\t} else {\n\t\terr = decompress(obj, outputPath, up)\n\t}\n\treturn err\n}\n\nvar _ tool.Tool = (*ISO9660)(nil)\n\nfunc init() {\n\ttool.RegisterTool(ISO9660{})\n}\n"
  },
  {
    "path": "internal/archive/iso9660/utils.go",
    "content": "package iso9660\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/kdomanski/iso9660\"\n)\n\nfunc getImage(ss *stream.SeekableStream) (*iso9660.Image, error) {\n\treader, err := stream.NewReadAtSeeker(ss, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn iso9660.OpenImage(reader)\n}\n\nfunc getObj(img *iso9660.Image, path string) (*iso9660.File, error) {\n\tobj, err := img.RootDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif path == \"/\" {\n\t\treturn obj, nil\n\t}\n\tpaths := strings.Split(strings.TrimPrefix(path, \"/\"), \"/\")\n\tfor _, p := range paths {\n\t\tif !obj.IsDir() {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t\tchildren, err := obj.GetChildren()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\texist := false\n\t\tfor _, child := range children {\n\t\t\tif child.Name() == p {\n\t\t\t\tobj = child\n\t\t\t\texist = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t}\n\treturn obj, nil\n}\n\nfunc toModelObj(file *iso9660.File) model.Obj {\n\treturn &model.Object{\n\t\tName:     file.Name(),\n\t\tSize:     file.Size(),\n\t\tModified: file.ModTime(),\n\t\tIsFolder: file.IsDir(),\n\t}\n}\n\nfunc decompress(f *iso9660.File, path string, up model.UpdateProgress) error {\n\tdestPath := filepath.Join(path, f.Name())\n\tif !strings.HasPrefix(destPath, path+string(os.PathSeparator)) {\n\t\treturn fmt.Errorf(\"illegal file path: %s\", f.Name())\n\t}\n\tfile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\t_, err = utils.CopyWithBuffer(file, &stream.ReaderUpdatingProgress{\n\t\tReader: &stream.SimpleReaderWithSize{\n\t\t\tReader: f.Reader(),\n\t\t\tSize:   f.Size(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\treturn err\n}\n\nfunc decompressAll(children []*iso9660.File, path string) error {\n\tfor _, child := range children {\n\t\tif child.IsDir() {\n\t\t\tnextChildren, err := child.GetChildren()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnextPath := filepath.Join(path, child.Name())\n\t\t\tif !strings.HasPrefix(nextPath, path+string(os.PathSeparator)) {\n\t\t\t\treturn fmt.Errorf(\"illegal file path: %s\", child.Name())\n\t\t\t}\n\t\t\tif err = os.MkdirAll(nextPath, 0700); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = decompressAll(nextChildren, nextPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err := decompress(child, path, func(_ float64) {}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/archive/rardecode/rardecode.go",
    "content": "package rardecode\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/nwaples/rardecode/v2\"\n)\n\ntype RarDecoder struct{}\n\nfunc (RarDecoder) AcceptedExtensions() []string {\n\treturn []string{\".rar\"}\n}\n\nfunc (RarDecoder) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{\n\t\t\".part1.rar\": {PartFileFormat: regexp.MustCompile(`^.*\\.part(\\d+)\\.rar$`), SecondPartIndex: 2},\n\t}\n}\n\nfunc (RarDecoder) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tl, err := list(ss, args.Password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, tree := tool.GenerateMetaTreeFromFolderTraversal(l)\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   \"\",\n\t\tEncrypted: false,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (RarDecoder) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (RarDecoder) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\treader, err := getReader(ss, args.Password)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\tfor {\n\t\tvar header *rardecode.FileHeader\n\t\theader, err = reader.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tif header.Name == innerPath {\n\t\t\tif header.IsDir {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn io.NopCloser(reader), header.UnPackedSize, nil\n\t\t}\n\t}\n\treturn nil, 0, errs.ObjectNotFound\n}\n\nfunc (RarDecoder) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\treader, err := getReader(ss, args.Password)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif args.InnerPath == \"/\" {\n\t\tfor {\n\t\t\tvar header *rardecode.FileHeader\n\t\t\theader, err = reader.Next()\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tname := header.Name\n\t\t\tif header.IsDir {\n\t\t\t\tname = name + \"/\"\n\t\t\t}\n\t\t\terr = decompress(reader, header, name, outputPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\t\tinnerBase := filepath.Base(innerPath)\n\t\tcreatedBaseDir := false\n\t\tfor {\n\t\t\tvar header *rardecode.FileHeader\n\t\t\theader, err = reader.Next()\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tname := header.Name\n\t\t\tif header.IsDir {\n\t\t\t\tname = name + \"/\"\n\t\t\t}\n\t\t\tif name == innerPath {\n\t\t\t\terr = _decompress(reader, header, outputPath, up)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t} else if strings.HasPrefix(name, innerPath+\"/\") {\n\t\t\t\ttargetPath := filepath.Join(outputPath, innerBase)\n\t\t\t\tif !createdBaseDir {\n\t\t\t\t\terr = os.Mkdir(targetPath, 0700)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcreatedBaseDir = true\n\t\t\t\t}\n\t\t\t\trestPath := strings.TrimPrefix(name, innerPath+\"/\")\n\t\t\t\terr = decompress(reader, header, restPath, targetPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nvar _ tool.Tool = (*RarDecoder)(nil)\n\nfunc init() {\n\ttool.RegisterTool(RarDecoder{})\n}\n"
  },
  {
    "path": "internal/archive/rardecode/utils.go",
    "content": "package rardecode\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/nwaples/rardecode/v2\"\n)\n\ntype VolumeFile struct {\n\tmodel.File\n\tname string\n\tss   model.FileStreamer\n}\n\nfunc (v *VolumeFile) Name() string {\n\treturn v.name\n}\n\nfunc (v *VolumeFile) Size() int64 {\n\treturn v.ss.GetSize()\n}\n\nfunc (v *VolumeFile) Mode() fs.FileMode {\n\treturn 0644\n}\n\nfunc (v *VolumeFile) ModTime() time.Time {\n\treturn v.ss.ModTime()\n}\n\nfunc (v *VolumeFile) IsDir() bool {\n\treturn false\n}\n\nfunc (v *VolumeFile) Sys() any {\n\treturn nil\n}\n\nfunc (v *VolumeFile) Stat() (fs.FileInfo, error) {\n\treturn v, nil\n}\n\nfunc (v *VolumeFile) Close() error {\n\treturn nil\n}\n\ntype VolumeFs struct {\n\tparts map[string]*VolumeFile\n}\n\nfunc (v *VolumeFs) Open(name string) (fs.File, error) {\n\tfile, ok := v.parts[name]\n\tif !ok {\n\t\treturn nil, fs.ErrNotExist\n\t}\n\treturn file, nil\n}\n\nfunc makeOpts(ss []*stream.SeekableStream) (string, rardecode.Option, error) {\n\tif len(ss) == 1 {\n\t\treader, err := stream.NewReadAtSeeker(ss[0], 0)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tfileName := \"file.rar\"\n\t\tfsys := &VolumeFs{parts: map[string]*VolumeFile{\n\t\t\tfileName: {File: reader, name: fileName},\n\t\t}}\n\t\treturn fileName, rardecode.FileSystem(fsys), nil\n\t} else {\n\t\tparts := make(map[string]*VolumeFile, len(ss))\n\t\tfor i, s := range ss {\n\t\t\treader, err := stream.NewReadAtSeeker(s, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t\tfileName := fmt.Sprintf(\"file.part%d.rar\", i+1)\n\t\t\tparts[fileName] = &VolumeFile{File: reader, name: fileName, ss: s}\n\t\t}\n\t\treturn \"file.part1.rar\", rardecode.FileSystem(&VolumeFs{parts: parts}), nil\n\t}\n}\n\ntype WrapReader struct {\n\tfiles []*rardecode.File\n}\n\nfunc (r *WrapReader) Files() []tool.SubFile {\n\tret := make([]tool.SubFile, 0, len(r.files))\n\tfor _, f := range r.files {\n\t\tret = append(ret, &WrapFile{File: f})\n\t}\n\treturn ret\n}\n\ntype WrapFile struct {\n\t*rardecode.File\n}\n\nfunc (f *WrapFile) Name() string {\n\tif f.File.IsDir {\n\t\treturn f.File.Name + \"/\"\n\t}\n\treturn f.File.Name\n}\n\nfunc (f *WrapFile) FileInfo() fs.FileInfo {\n\treturn &WrapFileInfo{File: f.File}\n}\n\ntype WrapFileInfo struct {\n\t*rardecode.File\n}\n\nfunc (f *WrapFileInfo) Name() string {\n\treturn filepath.Base(f.File.Name)\n}\n\nfunc (f *WrapFileInfo) Size() int64 {\n\treturn f.File.UnPackedSize\n}\n\nfunc (f *WrapFileInfo) ModTime() time.Time {\n\treturn f.File.ModificationTime\n}\n\nfunc (f *WrapFileInfo) IsDir() bool {\n\treturn f.File.IsDir\n}\n\nfunc (f *WrapFileInfo) Sys() any {\n\treturn nil\n}\n\nfunc list(ss []*stream.SeekableStream, password string) (*WrapReader, error) {\n\tfileName, fsOpt, err := makeOpts(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts := []rardecode.Option{fsOpt}\n\tif password != \"\" {\n\t\topts = append(opts, rardecode.Password(password))\n\t}\n\tfiles, err := rardecode.List(fileName, opts...)\n\t// rardecode输出文件列表的顺序不一定是父目录在前，子目录在后\n\t// 父路径的长度一定比子路径短，排序后的files可保证父路径在前\n\tsort.Slice(files, func(i, j int) bool {\n\t\treturn len(files[i].Name) < len(files[j].Name)\n\t})\n\tif err != nil {\n\t\treturn nil, filterPassword(err)\n\t}\n\treturn &WrapReader{files: files}, nil\n}\n\nfunc getReader(ss []*stream.SeekableStream, password string) (*rardecode.Reader, error) {\n\tfileName, fsOpt, err := makeOpts(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts := []rardecode.Option{fsOpt}\n\tif password != \"\" {\n\t\topts = append(opts, rardecode.Password(password))\n\t}\n\trc, err := rardecode.OpenReader(fileName, opts...)\n\tif err != nil {\n\t\treturn nil, filterPassword(err)\n\t}\n\tss[0].Closers.Add(rc)\n\treturn &rc.Reader, nil\n}\n\nfunc decompress(reader *rardecode.Reader, header *rardecode.FileHeader, filePath, outputPath string) error {\n\ttargetPath := outputPath\n\tdir, base := filepath.Split(filePath)\n\tif dir != \"\" {\n\t\ttargetPath = filepath.Join(targetPath, dir)\n\t\tif strings.HasPrefix(targetPath, outputPath+string(os.PathSeparator)) {\n\t\t\terr := os.MkdirAll(targetPath, 0700)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\ttargetPath = outputPath\n\t\t}\n\t}\n\tif base != \"\" {\n\t\terr := _decompress(reader, header, targetPath, func(_ float64) {})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc _decompress(reader *rardecode.Reader, header *rardecode.FileHeader, targetPath string, up model.UpdateProgress) error {\n\tdestPath := filepath.Join(targetPath, filepath.Base(header.Name))\n\tif !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {\n\t\treturn fmt.Errorf(\"illegal file path: %s\", filepath.Base(header.Name))\n\t}\n\tf, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = f.Close() }()\n\t_, err = io.Copy(f, &stream.ReaderUpdatingProgress{\n\t\tReader: &stream.SimpleReaderWithSize{\n\t\t\tReader: reader,\n\t\t\tSize:   header.UnPackedSize,\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc filterPassword(err error) error {\n\tif err != nil && strings.Contains(err.Error(), \"password\") {\n\t\treturn errs.WrongArchivePassword\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/archive/sevenzip/sevenzip.go",
    "content": "package sevenzip\n\nimport (\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n)\n\ntype SevenZip struct{}\n\nfunc (SevenZip) AcceptedExtensions() []string {\n\treturn []string{\".7z\"}\n}\n\nfunc (SevenZip) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{\n\t\t\".7z.001\": {PartFileFormat: regexp.MustCompile(`^.*\\.7z\\.(\\d+)$`), SecondPartIndex: 2},\n\t}\n}\n\nfunc (SevenZip) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treader, err := getReader(ss, args.Password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, tree := tool.GenerateMetaTreeFromFolderTraversal(&WrapReader{Reader: reader})\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   \"\",\n\t\tEncrypted: args.Password != \"\",\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (SevenZip) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (SevenZip) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\treader, err := getReader(ss, args.Password)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\tfor _, file := range reader.File {\n\t\tif file.Name == innerPath {\n\t\t\tr, e := file.Open()\n\t\t\tif e != nil {\n\t\t\t\treturn nil, 0, e\n\t\t\t}\n\t\t\treturn r, file.FileInfo().Size(), nil\n\t\t}\n\t}\n\treturn nil, 0, errs.ObjectNotFound\n}\n\nfunc (SevenZip) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\treader, err := getReader(ss, args.Password)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn tool.DecompressFromFolderTraversal(&WrapReader{Reader: reader}, outputPath, args, up)\n}\n\nvar _ tool.Tool = (*SevenZip)(nil)\n\nfunc init() {\n\ttool.RegisterTool(SevenZip{})\n}\n"
  },
  {
    "path": "internal/archive/sevenzip/utils.go",
    "content": "package sevenzip\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/bodgit/sevenzip\"\n)\n\ntype WrapReader struct {\n\tReader *sevenzip.Reader\n}\n\nfunc (r *WrapReader) Files() []tool.SubFile {\n\tret := make([]tool.SubFile, 0, len(r.Reader.File))\n\tfor _, f := range r.Reader.File {\n\t\tret = append(ret, &WrapFile{f: f})\n\t}\n\treturn ret\n}\n\ntype WrapFile struct {\n\tf *sevenzip.File\n}\n\nfunc (f *WrapFile) Name() string {\n\treturn f.f.Name\n}\n\nfunc (f *WrapFile) FileInfo() fs.FileInfo {\n\treturn f.f.FileInfo()\n}\n\nfunc (f *WrapFile) Open() (io.ReadCloser, error) {\n\treturn f.f.Open()\n}\n\nfunc getReader(ss []*stream.SeekableStream, password string) (*sevenzip.Reader, error) {\n\treaderAt, err := stream.NewMultiReaderAt(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsr, err := sevenzip.NewReaderWithPassword(readerAt, readerAt.Size(), password)\n\tif err != nil {\n\t\treturn nil, filterPassword(err)\n\t}\n\treturn sr, nil\n}\n\nfunc filterPassword(err error) error {\n\tif err != nil {\n\t\tvar e *sevenzip.ReadError\n\t\tif errors.As(err, &e) && e.Encrypted {\n\t\t\treturn errs.WrongArchivePassword\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/archive/tool/base.go",
    "content": "package tool\n\nimport (\n\t\"io\"\n\t\"regexp\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n)\n\ntype MultipartExtension struct {\n\tPartFileFormat  *regexp.Regexp\n\tSecondPartIndex int\n}\n\ntype Tool interface {\n\tAcceptedExtensions() []string\n\tAcceptedMultipartExtensions() map[string]MultipartExtension\n\tGetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error)\n\tList(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error)\n\tExtract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error)\n\tDecompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error\n}\n"
  },
  {
    "path": "internal/archive/tool/helper.go",
    "content": "package tool\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n)\n\ntype SubFile interface {\n\tName() string\n\tFileInfo() fs.FileInfo\n\tOpen() (io.ReadCloser, error)\n}\n\ntype CanEncryptSubFile interface {\n\tIsEncrypted() bool\n\tSetPassword(password string)\n}\n\ntype ArchiveReader interface {\n\tFiles() []SubFile\n}\n\nfunc GenerateMetaTreeFromFolderTraversal(r ArchiveReader) (bool, []model.ObjTree) {\n\tencrypted := false\n\tdirMap := make(map[string]*model.ObjectTree)\n\tfor _, file := range r.Files() {\n\t\tif encrypt, ok := file.(CanEncryptSubFile); ok && encrypt.IsEncrypted() {\n\t\t\tencrypted = true\n\t\t}\n\n\t\tname := strings.TrimPrefix(file.Name(), \"/\")\n\t\tvar dir string\n\t\tvar dirObj *model.ObjectTree\n\t\tisNewFolder := false\n\t\tif !file.FileInfo().IsDir() {\n\t\t\t// 先将 文件 添加到 所在的文件夹\n\t\t\tdir = filepath.Dir(name)\n\t\t\tdirObj = dirMap[dir]\n\t\t\tif dirObj == nil {\n\t\t\t\tisNewFolder = dir != \".\"\n\t\t\t\tdirObj = &model.ObjectTree{}\n\t\t\t\tdirObj.IsFolder = true\n\t\t\t\tdirObj.Name = filepath.Base(dir)\n\t\t\t\tdirObj.Modified = file.FileInfo().ModTime()\n\t\t\t\tdirMap[dir] = dirObj\n\t\t\t}\n\t\t\tdirObj.Children = append(\n\t\t\t\tdirObj.Children, &model.ObjectTree{\n\t\t\t\t\tObject: *MakeModelObj(file.FileInfo()),\n\t\t\t\t},\n\t\t\t)\n\t\t} else {\n\t\t\tdir = strings.TrimSuffix(name, \"/\")\n\t\t\tdirObj = dirMap[dir]\n\t\t\tif dirObj == nil {\n\t\t\t\tisNewFolder = dir != \".\"\n\t\t\t\tdirObj = &model.ObjectTree{}\n\t\t\t\tdirMap[dir] = dirObj\n\t\t\t}\n\t\t\tdirObj.IsFolder = true\n\t\t\tdirObj.Name = filepath.Base(dir)\n\t\t\tdirObj.Modified = file.FileInfo().ModTime()\n\t\t}\n\t\tif isNewFolder {\n\t\t\t// 将 文件夹 添加到 父文件夹\n\t\t\t// 考虑压缩包仅记录文件的路径，不记录文件夹\n\t\t\t// 循环创建所有父文件夹\n\t\t\tparentDir := filepath.Dir(dir)\n\t\t\tfor {\n\t\t\t\tparentDirObj := dirMap[parentDir]\n\t\t\t\tif parentDirObj == nil {\n\t\t\t\t\tparentDirObj = &model.ObjectTree{}\n\t\t\t\t\tif parentDir != \".\" {\n\t\t\t\t\t\tparentDirObj.IsFolder = true\n\t\t\t\t\t\tparentDirObj.Name = filepath.Base(parentDir)\n\t\t\t\t\t\tparentDirObj.Modified = file.FileInfo().ModTime()\n\t\t\t\t\t}\n\t\t\t\t\tdirMap[parentDir] = parentDirObj\n\t\t\t\t}\n\t\t\t\tparentDirObj.Children = append(parentDirObj.Children, dirObj)\n\n\t\t\t\tparentDir = filepath.Dir(parentDir)\n\t\t\t\tif dirMap[parentDir] != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdirObj = parentDirObj\n\t\t\t}\n\t\t}\n\t}\n\tif len(dirMap) > 0 {\n\t\treturn encrypted, dirMap[\".\"].GetChildren()\n\t} else {\n\t\treturn encrypted, nil\n\t}\n}\n\nfunc MakeModelObj(file os.FileInfo) *model.Object {\n\treturn &model.Object{\n\t\tName:     file.Name(),\n\t\tSize:     file.Size(),\n\t\tModified: file.ModTime(),\n\t\tIsFolder: file.IsDir(),\n\t}\n}\n\ntype WrapFileInfo struct {\n\tmodel.Obj\n}\n\nfunc DecompressFromFolderTraversal(r ArchiveReader, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\tvar err error\n\tfiles := r.Files()\n\tif args.InnerPath == \"/\" {\n\t\tfor i, file := range files {\n\t\t\tname := file.Name()\n\t\t\terr = decompress(file, name, outputPath, args.Password)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tup(float64(i+1) * 100.0 / float64(len(files)))\n\t\t}\n\t} else {\n\t\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\t\tinnerBase := filepath.Base(innerPath)\n\t\tcreatedBaseDir := false\n\t\tfor _, file := range files {\n\t\t\tname := file.Name()\n\t\t\tif name == innerPath {\n\t\t\t\terr = _decompress(file, outputPath, args.Password, up)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t} else if strings.HasPrefix(name, innerPath+\"/\") {\n\t\t\t\ttargetPath := filepath.Join(outputPath, innerBase)\n\t\t\t\tif !createdBaseDir {\n\t\t\t\t\terr = os.Mkdir(targetPath, 0700)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcreatedBaseDir = true\n\t\t\t\t}\n\t\t\t\trestPath := strings.TrimPrefix(name, innerPath+\"/\")\n\t\t\t\terr = decompress(file, restPath, targetPath, args.Password)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc decompress(file SubFile, filePath, outputPath, password string) error {\n\ttargetPath := outputPath\n\tdir, base := filepath.Split(filePath)\n\tif dir != \"\" {\n\t\ttargetPath = filepath.Join(targetPath, dir)\n\t\tif strings.HasPrefix(targetPath, outputPath+string(os.PathSeparator)) {\n\t\t\terr := os.MkdirAll(targetPath, 0700)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\ttargetPath = outputPath\n\t\t}\n\t}\n\tif base != \"\" {\n\t\terr := _decompress(file, targetPath, password, func(_ float64) {})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc _decompress(file SubFile, targetPath, password string, up model.UpdateProgress) error {\n\tif encrypt, ok := file.(CanEncryptSubFile); ok && encrypt.IsEncrypted() {\n\t\tencrypt.SetPassword(password)\n\t}\n\trc, err := file.Open()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = rc.Close() }()\n\tdestPath := filepath.Join(targetPath, file.FileInfo().Name())\n\tif !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {\n\t\treturn fmt.Errorf(\"illegal file path: %s\", file.FileInfo().Name())\n\t}\n\tf, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = f.Close() }()\n\t_, err = io.Copy(f, &stream.ReaderUpdatingProgress{\n\t\tReader: &stream.SimpleReaderWithSize{\n\t\t\tReader: rc,\n\t\t\tSize:   file.FileInfo().Size(),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/archive/tool/utils.go",
    "content": "package tool\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n)\n\nvar (\n\tTools               = make(map[string]Tool)\n\tMultipartExtensions = make(map[string]MultipartExtension)\n)\n\nfunc RegisterTool(tool Tool) {\n\tfor _, ext := range tool.AcceptedExtensions() {\n\t\tTools[ext] = tool\n\t}\n\tfor mainFile, ext := range tool.AcceptedMultipartExtensions() {\n\t\tMultipartExtensions[mainFile] = ext\n\t\tTools[mainFile] = tool\n\t}\n}\n\nfunc GetArchiveTool(ext string) (*MultipartExtension, Tool, error) {\n\tt, ok := Tools[ext]\n\tif !ok {\n\t\treturn nil, nil, errs.UnknownArchiveFormat\n\t}\n\tpartExt, ok := MultipartExtensions[ext]\n\tif !ok {\n\t\treturn nil, t, nil\n\t}\n\treturn &partExt, t, nil\n}\n"
  },
  {
    "path": "internal/archive/zip/utils.go",
    "content": "package zip\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"io/fs\"\n\t\"strings\"\n\n\t\"github.com/KirCute/zip\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"golang.org/x/text/encoding/ianaindex\"\n\t\"golang.org/x/text/transform\"\n)\n\ntype WrapReader struct {\n\tReader *zip.Reader\n}\n\nfunc (r *WrapReader) Files() []tool.SubFile {\n\tret := make([]tool.SubFile, 0, len(r.Reader.File))\n\tfor _, f := range r.Reader.File {\n\t\tret = append(ret, &WrapFile{f: f})\n\t}\n\treturn ret\n}\n\ntype WrapFileInfo struct {\n\tfs.FileInfo\n\tefs bool\n}\n\nfunc (f *WrapFileInfo) Name() string {\n\treturn decodeName(f.FileInfo.Name(), f.efs)\n}\n\ntype WrapFile struct {\n\tf *zip.File\n}\n\nfunc (f *WrapFile) Name() string {\n\treturn decodeName(f.f.Name, isEFS(f.f.Flags))\n}\n\nfunc (f *WrapFile) FileInfo() fs.FileInfo {\n\treturn &WrapFileInfo{FileInfo: f.f.FileInfo(), efs: isEFS(f.f.Flags)}\n}\n\nfunc (f *WrapFile) Open() (io.ReadCloser, error) {\n\treturn f.f.Open()\n}\n\nfunc (f *WrapFile) IsEncrypted() bool {\n\treturn f.f.IsEncrypted()\n}\n\nfunc (f *WrapFile) SetPassword(password string) {\n\tf.f.SetPassword(password)\n}\n\nfunc makePart(ss *stream.SeekableStream) (zip.SizeReaderAt, error) {\n\tra, err := stream.NewReadAtSeeker(ss, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &inlineSizeReaderAt{ReaderAt: ra, size: ss.GetSize()}, nil\n}\n\nfunc (z *Zip) getReader(ss []*stream.SeekableStream) (*zip.Reader, error) {\n\tif len(ss) > 1 && z.traditionalSecondPartRegExp.MatchString(ss[1].GetName()) {\n\t\tss = append(ss[1:], ss[0])\n\t\tras := make([]zip.SizeReaderAt, 0, len(ss))\n\t\tfor _, s := range ss {\n\t\t\tra, err := makePart(s)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tras = append(ras, ra)\n\t\t}\n\t\treturn zip.NewMultipartReader(ras)\n\t} else {\n\t\treader, err := stream.NewMultiReaderAt(ss)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn zip.NewReader(reader, reader.Size())\n\t}\n}\n\nfunc filterPassword(err error) error {\n\tif err != nil && strings.Contains(err.Error(), \"password\") {\n\t\treturn errs.WrongArchivePassword\n\t}\n\treturn err\n}\n\nfunc decodeName(name string, efs bool) string {\n\tif efs {\n\t\treturn name\n\t}\n\tenc, err := ianaindex.IANA.Encoding(setting.GetStr(conf.NonEFSZipEncoding))\n\tif err != nil {\n\t\treturn name\n\t}\n\ti := bytes.NewReader([]byte(name))\n\tdecoder := transform.NewReader(i, enc.NewDecoder())\n\tcontent, _ := io.ReadAll(decoder)\n\treturn string(content)\n}\n\nfunc isEFS(flags uint16) bool {\n\treturn (flags & 0x800) > 0\n}\n\ntype inlineSizeReaderAt struct {\n\tio.ReaderAt\n\tsize int64\n}\n\nfunc (i *inlineSizeReaderAt) Size() int64 {\n\treturn i.size\n}\n"
  },
  {
    "path": "internal/archive/zip/zip.go",
    "content": "package zip\n\nimport (\n\t\"io\"\n\tstdpath \"path\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n)\n\ntype Zip struct {\n\ttraditionalSecondPartRegExp *regexp.Regexp\n}\n\nfunc (z *Zip) AcceptedExtensions() []string {\n\treturn []string{}\n}\n\nfunc (z *Zip) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{\n\t\t\".zip\":     {PartFileFormat: regexp.MustCompile(`^.*\\.z(\\d+)$`), SecondPartIndex: 1},\n\t\t\".zip.001\": {PartFileFormat: regexp.MustCompile(`^.*\\.zip\\.(\\d+)$`), SecondPartIndex: 2},\n\t}\n}\n\nfunc (z *Zip) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tzipReader, err := z.getReader(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tefs := true\n\tif len(zipReader.File) > 0 {\n\t\tefs = isEFS(zipReader.File[0].Flags)\n\t}\n\tencrypted, tree := tool.GenerateMetaTreeFromFolderTraversal(&WrapReader{Reader: zipReader})\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   decodeName(zipReader.Comment, efs),\n\t\tEncrypted: encrypted,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (z *Zip) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tzipReader, err := z.getReader(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif args.InnerPath == \"/\" {\n\t\tret := make([]model.Obj, 0)\n\t\tpassVerified := false\n\t\tvar dir *model.Object\n\t\tfor _, file := range zipReader.File {\n\t\t\tif !passVerified && file.IsEncrypted() {\n\t\t\t\tfile.SetPassword(args.Password)\n\t\t\t\trc, e := file.Open()\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn nil, filterPassword(e)\n\t\t\t\t}\n\t\t\t\t_ = rc.Close()\n\t\t\t\tpassVerified = true\n\t\t\t}\n\t\t\tname := strings.TrimSuffix(decodeName(file.Name, isEFS(file.Flags)), \"/\")\n\t\t\tif strings.Contains(name, \"/\") {\n\t\t\t\t// 有些压缩包不压缩第一个文件夹\n\t\t\t\tstrs := strings.Split(name, \"/\")\n\t\t\t\tif dir == nil && len(strs) == 2 {\n\t\t\t\t\tdir = &model.Object{\n\t\t\t\t\t\tName:     strs[0],\n\t\t\t\t\t\tModified: ss[0].ModTime(),\n\t\t\t\t\t\tIsFolder: true,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tret = append(ret, tool.MakeModelObj(&WrapFileInfo{FileInfo: file.FileInfo(), efs: isEFS(file.Flags)}))\n\t\t}\n\t\tif len(ret) == 0 && dir != nil {\n\t\t\tret = append(ret, dir)\n\t\t}\n\t\treturn ret, nil\n\t} else {\n\t\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\") + \"/\"\n\t\tret := make([]model.Obj, 0)\n\t\texist := false\n\t\tfor _, file := range zipReader.File {\n\t\t\tname := decodeName(file.Name, isEFS(file.Flags))\n\t\t\tdir := stdpath.Dir(strings.TrimSuffix(name, \"/\")) + \"/\"\n\t\t\tif dir != innerPath {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\texist = true\n\t\t\tret = append(ret, tool.MakeModelObj(&WrapFileInfo{file.FileInfo(), isEFS(file.Flags)}))\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t\treturn ret, nil\n\t}\n}\n\nfunc (z *Zip) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\tzipReader, err := z.getReader(ss)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tinnerPath := strings.TrimPrefix(args.InnerPath, \"/\")\n\tfor _, file := range zipReader.File {\n\t\tif decodeName(file.Name, isEFS(file.Flags)) == innerPath {\n\t\t\tif file.IsEncrypted() {\n\t\t\t\tfile.SetPassword(args.Password)\n\t\t\t}\n\t\t\tr, e := file.Open()\n\t\t\tif e != nil {\n\t\t\t\treturn nil, 0, e\n\t\t\t}\n\t\t\treturn r, file.FileInfo().Size(), nil\n\t\t}\n\t}\n\treturn nil, 0, errs.ObjectNotFound\n}\n\nfunc (z *Zip) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\tzipReader, err := z.getReader(ss)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn tool.DecompressFromFolderTraversal(&WrapReader{Reader: zipReader}, outputPath, args, up)\n}\n\nvar _ tool.Tool = (*Zip)(nil)\n\nfunc init() {\n\ttool.RegisterTool(&Zip{\n\t\ttraditionalSecondPartRegExp: regexp.MustCompile(`^.*\\.z0*1$`),\n\t})\n}\n"
  },
  {
    "path": "internal/authn/authn.go",
    "content": "package authn\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n)\n\nfunc NewAuthnInstance(c *gin.Context) (*webauthn.WebAuthn, error) {\n\tsiteUrl, err := url.Parse(common.GetApiUrl(c.Request.Context()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn webauthn.New(&webauthn.Config{\n\t\tRPDisplayName: setting.GetStr(conf.SiteTitle),\n\t\tRPID:          siteUrl.Hostname(),\n\t\t//RPOrigin:      siteUrl.String(),\n\t\tRPOrigins: []string{fmt.Sprintf(\"%s://%s\", siteUrl.Scheme, siteUrl.Host)},\n\t\t// RPOrigin: \"http://localhost:5173\"\n\t})\n}\n"
  },
  {
    "path": "internal/bootstrap/config.go",
    "content": "package bootstrap\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/caarlos0/env/v9\"\n\t\"github.com/shirou/gopsutil/v4/mem\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Program working directory\nfunc PWD() string {\n\tif flags.ForceBinDir {\n\t\tex, err := os.Executable()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tpwd := filepath.Dir(ex)\n\t\treturn pwd\n\t}\n\td, err := os.Getwd()\n\tif err != nil {\n\t\td = \".\"\n\t}\n\treturn d\n}\n\nfunc InitConfig() {\n\tpwd := PWD()\n\tdataDir := flags.DataDir\n\tif !filepath.IsAbs(dataDir) {\n\t\tflags.DataDir = filepath.Join(pwd, flags.DataDir)\n\t}\n\t// Determine config file path: use flags.ConfigPath if provided, otherwise default to <dataDir>/config.json\n\tconfigPath := flags.ConfigPath\n\tif configPath == \"\" {\n\t\tconfigPath = filepath.Join(flags.DataDir, \"config.json\")\n\t} else {\n\t\t// if relative, resolve relative to working directory\n\t\tif !filepath.IsAbs(configPath) {\n\t\t\tif absPath, err := filepath.Abs(configPath); err == nil {\n\t\t\t\tconfigPath = absPath\n\t\t\t} else {\n\t\t\t\tconfigPath = filepath.Join(pwd, configPath)\n\t\t\t}\n\t\t}\n\t}\n\tconfigPath = filepath.Clean(configPath)\n\tconf.ConfigPath = configPath\n\tlog.Infof(\"reading config file: %s\", configPath)\n\tif !utils.Exists(configPath) {\n\t\tlog.Infof(\"config file not exists, creating default config file\")\n\t\t_, err := utils.CreateNestedFile(configPath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to create config file: %+v\", err)\n\t\t}\n\t\tconf.Conf = conf.DefaultConfig(dataDir)\n\t\tLastLaunchedVersion = conf.Version\n\t\tconf.Conf.LastLaunchedVersion = conf.Version\n\t\tif !utils.WriteJsonToFile(configPath, conf.Conf) {\n\t\t\tlog.Fatalf(\"failed to create default config file\")\n\t\t}\n\t} else {\n\t\tconfigBytes, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"reading config file error: %+v\", err)\n\t\t}\n\t\tconf.Conf = conf.DefaultConfig(dataDir)\n\t\terr = utils.Json.Unmarshal(configBytes, conf.Conf)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"load config error: %+v\", err)\n\t\t}\n\t\tLastLaunchedVersion = conf.Conf.LastLaunchedVersion\n\t\tif strings.HasPrefix(conf.Version, \"v\") || LastLaunchedVersion == \"\" {\n\t\t\tconf.Conf.LastLaunchedVersion = conf.Version\n\t\t}\n\t\t// update config.json struct\n\t\tconfBody, err := utils.Json.MarshalIndent(conf.Conf, \"\", \"  \")\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"marshal config error: %+v\", err)\n\t\t}\n\t\terr = os.WriteFile(configPath, confBody, 0o777)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"update config struct error: %+v\", err)\n\t\t}\n\t}\n\tif !conf.Conf.Force {\n\t\tconfFromEnv()\n\t}\n\n\tif conf.Conf.MaxConcurrency > 0 {\n\t\tnet.DefaultConcurrencyLimit = &net.ConcurrencyLimit{Limit: conf.Conf.MaxConcurrency}\n\t}\n\tif conf.Conf.MaxBufferLimit < 0 {\n\t\tm, _ := mem.VirtualMemory()\n\t\tif m != nil {\n\t\t\tconf.MaxBufferLimit = max(int(float64(m.Total)*0.05), 4*utils.MB)\n\t\t\tconf.MaxBufferLimit -= conf.MaxBufferLimit % utils.MB\n\t\t} else {\n\t\t\tconf.MaxBufferLimit = 16 * utils.MB\n\t\t}\n\t} else {\n\t\tconf.MaxBufferLimit = conf.Conf.MaxBufferLimit * utils.MB\n\t}\n\tlog.Infof(\"max buffer limit: %dMB\", conf.MaxBufferLimit/utils.MB)\n\tif conf.Conf.MmapThreshold > 0 {\n\t\tconf.MmapThreshold = conf.Conf.MmapThreshold * utils.MB\n\t} else {\n\t\tconf.MmapThreshold = 0\n\t}\n\tlog.Infof(\"mmap threshold: %dMB\", conf.Conf.MmapThreshold)\n\n\tif len(conf.Conf.Log.Filter.Filters) == 0 {\n\t\tconf.Conf.Log.Filter.Enable = false\n\t}\n\t// convert abs path\n\tconvertAbsPath := func(path *string) {\n\t\tif *path != \"\" && !filepath.IsAbs(*path) {\n\t\t\t*path = filepath.Join(pwd, *path)\n\t\t}\n\t}\n\tconvertAbsPath(&conf.Conf.Database.DBFile)\n\tconvertAbsPath(&conf.Conf.Scheme.CertFile)\n\tconvertAbsPath(&conf.Conf.Scheme.KeyFile)\n\tconvertAbsPath(&conf.Conf.Scheme.UnixFile)\n\tconvertAbsPath(&conf.Conf.Log.Name)\n\tconvertAbsPath(&conf.Conf.TempDir)\n\tconvertAbsPath(&conf.Conf.BleveDir)\n\tconvertAbsPath(&conf.Conf.DistDir)\n\n\terr := os.MkdirAll(conf.Conf.TempDir, 0o777)\n\tif err != nil {\n\t\tlog.Fatalf(\"create temp dir error: %+v\", err)\n\t}\n\tlog.Debugf(\"config: %+v\", conf.Conf)\n\n\t// Validate and display proxy configuration status\n\tvalidateProxyConfig()\n\n\tbase.InitClient()\n\tinitURL()\n}\n\nfunc confFromEnv() {\n\tprefix := \"OPENLIST_\"\n\tif flags.NoPrefix {\n\t\tprefix = \"\"\n\t}\n\tlog.Infof(\"load config from env with prefix: %s\", prefix)\n\tif err := env.ParseWithOptions(conf.Conf, env.Options{\n\t\tPrefix: prefix,\n\t}); err != nil {\n\t\tlog.Fatalf(\"load config from env error: %+v\", err)\n\t}\n}\n\nfunc initURL() {\n\tif !strings.Contains(conf.Conf.SiteURL, \"://\") {\n\t\tconf.Conf.SiteURL = utils.FixAndCleanPath(conf.Conf.SiteURL)\n\t}\n\tu, err := url.Parse(conf.Conf.SiteURL)\n\tif err != nil {\n\t\tutils.Log.Fatalf(\"can't parse site_url: %+v\", err)\n\t}\n\tconf.URL = u\n}\n\nfunc CleanTempDir() {\n\tfiles, err := os.ReadDir(conf.Conf.TempDir)\n\tif err != nil {\n\t\tlog.Errorln(\"failed list temp file: \", err)\n\t}\n\tfor _, file := range files {\n\t\tif err := os.RemoveAll(filepath.Join(conf.Conf.TempDir, file.Name())); err != nil {\n\t\t\tlog.Errorln(\"failed delete temp file: \", err)\n\t\t}\n\t}\n}\n\n// validateProxyConfig validates proxy configuration and displays status at startup\nfunc validateProxyConfig() {\n\tif conf.Conf.ProxyAddress != \"\" {\n\t\tif _, err := url.Parse(conf.Conf.ProxyAddress); err == nil {\n\t\t\tlog.Infof(\"Proxy enabled: %s\", conf.Conf.ProxyAddress)\n\t\t} else {\n\t\t\tlog.Errorf(\"Invalid proxy address format: %s, error: %v\", conf.Conf.ProxyAddress, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/data/data.go",
    "content": "package data\n\nimport \"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\nfunc InitData() {\n\tinitUser()\n\tinitSettings()\n\tinitTasks()\n\tif flags.Dev {\n\t\tinitDevData()\n\t\tinitDevDo()\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/data/dev.go",
    "content": "package data\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/message\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc initDevData() {\n\t_, err := op.CreateStorage(context.Background(), model.Storage{\n\t\tMountPath: \"/\",\n\t\tOrder:     0,\n\t\tDriver:    \"Local\",\n\t\tStatus:    \"\",\n\t\tAddition:  `{\"root_folder_path\":\".\"}`,\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to create storage: %+v\", err)\n\t}\n\terr = db.CreateUser(&model.User{\n\t\tUsername:   \"Noah\",\n\t\tPassword:   \"hsu\",\n\t\tBasePath:   \"/data\",\n\t\tRole:       0,\n\t\tPermission: 512,\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to create user: %+v\", err)\n\t}\n}\n\nfunc initDevDo() {\n\tif flags.Dev {\n\t\tgo func() {\n\t\t\terr := message.GetMessenger().WaitSend(message.Message{\n\t\t\t\tType:    \"string\",\n\t\t\t\tContent: \"dev mode\",\n\t\t\t}, 10)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"%+v\", err)\n\t\t\t}\n\t\t\tm, err := message.GetMessenger().WaitReceive(10)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"%+v\", err)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"received: %+v\", m)\n\t\t\t}\n\t\t}()\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/data/setting.go",
    "content": "package data\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nfunc initSettings() {\n\tinitialSettingItems := InitialSettings()\n\tisActive := func(key string) bool {\n\t\tfor _, item := range initialSettingItems {\n\t\t\tif item.Key == key {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\t// check deprecated\n\tsettings, err := op.GetSettingItems()\n\tif err != nil {\n\t\tutils.Log.Fatalf(\"failed get settings: %+v\", err)\n\t}\n\tsettingMap := map[string]*model.SettingItem{}\n\tfor _, v := range settings {\n\t\tif !isActive(v.Key) && v.Flag != model.DEPRECATED {\n\t\t\tv.Flag = model.DEPRECATED\n\t\t\terr = op.SaveSettingItem(&v)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Fatalf(\"failed save setting: %+v\", err)\n\t\t\t}\n\t\t}\n\t\tsettingMap[v.Key] = &v\n\t}\n\top.MigrationSettingItems = map[string]op.MigrationValueItem{}\n\t// create or save setting\n\tvar saveItems []model.SettingItem\n\tfor i := range initialSettingItems {\n\t\titem := &initialSettingItems[i]\n\t\titem.Index = uint(i)\n\t\tmigrationValue := item.MigrationValue\n\t\tif len(migrationValue) > 0 {\n\t\t\top.MigrationSettingItems[item.Key] = op.MigrationValueItem{MigrationValue: item.MigrationValue, Value: item.Value}\n\t\t\titem.MigrationValue = \"\"\n\t\t}\n\t\t// err\n\t\tstored, ok := settingMap[item.Key]\n\t\tif !ok {\n\t\t\tstored, err = op.GetSettingItemByKey(item.Key)\n\t\t\tif err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\t\tutils.Log.Fatalf(\"failed get setting: %+v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif item.Key != conf.VERSION && stored != nil &&\n\t\t\t(len(migrationValue) == 0 || stored.Value != migrationValue) {\n\t\t\titem.Value = stored.Value\n\t\t}\n\t\t_, err = op.HandleSettingItemHook(item)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to execute hook on %s: %+v\", item.Key, err)\n\t\t\tcontinue\n\t\t}\n\t\tif stored == nil || *item != *stored {\n\t\t\tsaveItems = append(saveItems, *item)\n\t\t}\n\t}\n\tif len(saveItems) > 0 {\n\t\terr = db.SaveSettingItems(saveItems)\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"failed save setting: %+v\", err)\n\t\t} else {\n\t\t\top.SettingCacheUpdate()\n\t\t}\n\t}\n}\n\nfunc InitialSettings() []model.SettingItem {\n\tvar token string\n\tif flags.Dev {\n\t\ttoken = \"dev_token\"\n\t} else {\n\t\ttoken = random.Token()\n\t}\n\tsiteVersion := fmt.Sprintf(\"%s (Commit: %s) - Frontend: %s - Build at: %s\", conf.Version, conf.GitCommit, conf.WebVersion, conf.BuiltAt)\n\tinitialSettingItems := []model.SettingItem{\n\t\t// site settings\n\t\t{Key: conf.VERSION, Value: siteVersion, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},\n\t\t//{Key: conf.ApiUrl, Value: \"\", Type: conf.TypeString, Group: model.SITE},\n\t\t//{Key: conf.BasePath, Value: \"\", Type: conf.TypeString, Group: model.SITE},\n\t\t{Key: conf.SiteTitle, Value: \"OpenList\", Type: conf.TypeString, Group: model.SITE},\n\t\t{Key: conf.Announcement, Value: \"Welcome to the OpenList project!\\nFor the latest updates, to contribute code, or to submit suggestions and issues, please visit our [project repository](https://github.com/OpenListTeam/OpenList).\", Type: conf.TypeText, Group: model.SITE},\n\t\t{Key: \"pagination_type\", Value: \"all\", Type: conf.TypeSelect, Options: \"all,pagination,load_more,auto_load_more\", Group: model.SITE},\n\t\t{Key: \"default_page_size\", Value: \"30\", Type: conf.TypeNumber, Group: model.SITE},\n\t\t{Key: conf.AllowIndexed, Value: \"false\", Type: conf.TypeBool, Group: model.SITE},\n\t\t{Key: conf.AllowMounted, Value: \"true\", Type: conf.TypeBool, Group: model.SITE},\n\t\t{Key: conf.RobotsTxt, Value: \"User-agent: *\\nAllow: /\", Type: conf.TypeText, Group: model.SITE},\n\t\t// style settings\n\t\t{Key: conf.Logo, Value: \"https://res.oplist.org/logo/logo.svg\", MigrationValue: \"https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg\", Type: conf.TypeText, Group: model.STYLE},\n\t\t{Key: conf.Favicon, Value: \"https://res.oplist.org/logo/logo.svg\", MigrationValue: \"https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg\", Type: conf.TypeString, Group: model.STYLE},\n\t\t{Key: conf.MainColor, Value: \"#1890ff\", Type: conf.TypeString, Group: model.STYLE},\n\t\t{Key: \"home_icon\", Value: \"🏠\", Type: conf.TypeString, Group: model.STYLE},\n\t\t{Key: \"share_icon\", Value: \"🎁\", Type: conf.TypeString, Group: model.STYLE},\n\t\t{Key: \"home_container\", Value: \"max_980px\", Type: conf.TypeSelect, Options: \"max_980px,hope_container\", Group: model.STYLE},\n\t\t{Key: \"settings_layout\", Value: \"list\", Type: conf.TypeSelect, Options: \"list,responsive\", Group: model.STYLE},\n\t\t{Key: conf.HideStorageDetails, Value: \"true\", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PRIVATE},\n\t\t{Key: conf.HideStorageDetailsInManagePage, Value: \"true\", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PRIVATE},\n\t\t{Key: \"show_disk_usage_in_plain_text\", Value: \"false\", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PUBLIC},\n\t\t// preview settings\n\t\t{Key: conf.TextTypes, Value: \"txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: conf.AudioTypes, Value: \"mp3,flac,ogg,m4a,wav,opus,wma\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: conf.VideoTypes, Value: \"mp4,mkv,avi,mov,rmvb,webm,flv,m3u8\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: conf.ImageTypes, Value: \"jpg,tiff,jpeg,png,gif,bmp,svg,ico,swf,webp,avif\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t//{Key: conf.OfficeTypes, Value: \"doc,docx,xls,xlsx,ppt,pptx\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: conf.ProxyTypes, Value: \"m3u8,url\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: conf.ProxyIgnoreHeaders, Value: \"authorization,referer\", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},\n\t\t{Key: \"external_previews\", Value: `{}`, Type: conf.TypeText, Group: model.PREVIEW},\n\t\t{Key: \"iframe_previews\", Value: `{\n\t\"doc,docx,xls,xlsx,ppt,pptx\": {\n\t\t\"Microsoft\":\"https://view.officeapps.live.com/op/view.aspx?src=$e_url\",\n\t\t\"Google\":\"https://docs.google.com/gview?url=$e_url&embedded=true\"\n\t},\n\t\"pdf\": {\n\t\t\"PDF.js\":\"https://res.oplist.org/pdf.js/web/viewer.html?file=$e_url\"\n\t},\n\t\"epub\": {\n\t\t\"EPUB.js\":\"https://res.oplist.org/epub.js/viewer.html?url=$e_url\"\n\t}\n}`, Type: conf.TypeText, Group: model.PREVIEW},\n\t\t//\t\t{Key: conf.OfficeViewers, Value: `{\n\t\t//\t\"Microsoft\":\"https://view.officeapps.live.com/op/view.aspx?src=$url\",\n\t\t//\t\"Google\":\"https://docs.google.com/gview?url=$url&embedded=true\",\n\t\t//}`, Type: conf.TypeText, Group: model.PREVIEW},\n\t\t//\t\t{Key: conf.PdfViewers, Value: `{\n\t\t//\t\"pdf.js\":\"https://openlistteam.github.io/pdf.js/web/viewer.html?file=$url\"\n\t\t//}`, Type: conf.TypeText, Group: model.PREVIEW},\n\t\t{Key: \"audio_cover\", Value: \"https://res.oplist.org/logo/logo.svg\", MigrationValue: \"https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg\", Type: conf.TypeString, Group: model.PREVIEW},\n\t\t{Key: conf.AudioAutoplay, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.VideoAutoplay, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.PreviewDownloadByDefault, Value: \"false\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.PreviewArchivesByDefault, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.SharePreviewDownloadByDefault, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.SharePreviewArchivesByDefault, Value: \"false\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.ReadMeAutoRender, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.FilterReadMeScripts, Value: \"true\", Type: conf.TypeBool, Group: model.PREVIEW},\n\t\t{Key: conf.NonEFSZipEncoding, Value: \"IBM437\", Type: conf.TypeString, Group: model.PREVIEW},\n\t\t// global settings\n\t\t{Key: conf.HideFiles, Value: \"/\\\\/README.md/i\", Type: conf.TypeText, Group: model.GLOBAL},\n\t\t{Key: \"package_download\", Value: \"true\", Type: conf.TypeBool, Group: model.GLOBAL},\n\t\t{Key: conf.CustomizeHead, MigrationValue: `<script src=\"https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=String.prototype.replaceAll\"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.LinkExpiration, Value: \"0\", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.SignAll, Value: \"true\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{\n\t\t\tKey: conf.PrivacyRegs, Value: `(?:(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\n([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)\n(?U)access_token=(.*)&`,\n\t\t\tType: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE,\n\t\t},\n\t\t{Key: conf.OcrApi, Value: \"https://openlistteam-ocr-api-server.hf.space/ocr/file/json\", MigrationValue: \"https://api.example.com/ocr/file/json\", Type: conf.TypeString, Group: model.GLOBAL}, // TODO: This can be replace by a community-hosted endpoint, see https://github.com/OpenListTeam/ocr_api_server\n\t\t{Key: conf.FilenameCharMapping, Value: `{\"/\": \"|\"}`, Type: conf.TypeText, Group: model.GLOBAL},\n\t\t{Key: conf.ForwardDirectLinkParams, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL},\n\t\t{Key: conf.IgnoreDirectLinkParams, Value: \"sign,openlist_ts,raw\", Type: conf.TypeString, Group: model.GLOBAL},\n\t\t{Key: conf.WebauthnLoginEnabled, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},\n\t\t{Key: conf.SharePreview, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},\n\t\t{Key: conf.ShareArchivePreview, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},\n\t\t{Key: conf.ShareForceProxy, Value: \"true\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.ShareSummaryContent, Value: \"@{{creator}} shared {{#each files}}{{#if @first}}\\\"{{filename this}}\\\"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}\", Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PUBLIC},\n\t\t{Key: conf.HandleHookAfterWriting, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.HandleHookRateLimit, Value: \"0\", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},\n\t\t{Key: conf.IgnoreSystemFiles, Value: \"false\", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE, Help: `When enabled, ignores common system files during upload (.DS_Store, desktop.ini, Thumbs.db, and files starting with ._)`},\n\n\t\t// single settings\n\t\t{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},\n\t\t{Key: conf.SearchIndex, Value: \"none\", Type: conf.TypeSelect, Options: \"database,database_non_full_text,bleve,meilisearch,none\", Group: model.INDEX},\n\t\t{Key: conf.AutoUpdateIndex, Value: \"false\", Type: conf.TypeBool, Group: model.INDEX},\n\t\t{Key: conf.IgnorePaths, Value: \"\", Type: conf.TypeText, Group: model.INDEX, Flag: model.PRIVATE, Help: `one path per line`},\n\t\t{Key: conf.MaxIndexDepth, Value: \"20\", Type: conf.TypeNumber, Group: model.INDEX, Flag: model.PRIVATE, Help: `max depth of index`},\n\t\t{Key: conf.IndexProgress, Value: \"{}\", Type: conf.TypeText, Group: model.SINGLE, Flag: model.PRIVATE},\n\n\t\t// SSO settings\n\t\t{Key: conf.SSOLoginEnabled, Value: \"false\", Type: conf.TypeBool, Group: model.SSO, Flag: model.PUBLIC},\n\t\t{Key: conf.SSOLoginPlatform, Type: conf.TypeSelect, Options: \"Casdoor,Github,Microsoft,Google,Dingtalk,OIDC\", Group: model.SSO, Flag: model.PUBLIC},\n\t\t{Key: conf.SSOClientId, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOClientSecret, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOOIDCUsernameKey, Value: \"name\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOOrganizationName, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOApplicationName, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOEndpointName, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOJwtPublicKey, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOExtraScopes, Value: \"\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOAutoRegister, Value: \"false\", Type: conf.TypeBool, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSODefaultDir, Value: \"/\", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSODefaultPermission, Value: \"0\", Type: conf.TypeNumber, Group: model.SSO, Flag: model.PRIVATE},\n\t\t{Key: conf.SSOCompatibilityMode, Value: \"false\", Type: conf.TypeBool, Group: model.SSO, Flag: model.PUBLIC},\n\n\t\t// ldap settings\n\t\t{Key: conf.LdapLoginEnabled, Value: \"false\", Type: conf.TypeBool, Group: model.LDAP, Flag: model.PUBLIC},\n\t\t{Key: conf.LdapServer, Value: \"\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapSkipTlsVerify, Value: \"false\", Type: conf.TypeBool, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapManagerDN, Value: \"\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapManagerPassword, Value: \"\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapUserSearchBase, Value: \"\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapUserSearchFilter, Value: \"(uid=%s)\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapDefaultDir, Value: \"/\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapDefaultPermission, Value: \"0\", Type: conf.TypeNumber, Group: model.LDAP, Flag: model.PRIVATE},\n\t\t{Key: conf.LdapLoginTips, Value: \"login with ldap\", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC},\n\n\t\t// s3 settings\n\t\t{Key: conf.S3AccessKeyId, Value: \"\", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},\n\t\t{Key: conf.S3SecretAccessKey, Value: \"\", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},\n\t\t{Key: conf.S3Buckets, Value: \"[]\", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},\n\n\t\t// ftp settings\n\t\t{Key: conf.FTPPublicHost, Value: \"127.0.0.1\", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.FTPPasvPortMap, Value: \"\", Type: conf.TypeText, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.FTPMandatoryTLS, Value: \"false\", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.FTPImplicitTLS, Value: \"false\", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.FTPTLSPrivateKeyPath, Value: \"\", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.FTPTLSPublicCertPath, Value: \"\", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},\n\t\t{Key: conf.SFTPDisablePasswordLogin, Value: \"false\", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},\n\n\t\t// traffic settings\n\t\t{Key: conf.TaskOfflineDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Download.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.TaskOfflineDownloadTransferThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Transfer.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.TaskUploadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Upload.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.TaskCopyThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Copy.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.TaskDecompressDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Decompress.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.TaskDecompressUploadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.DecompressUpload.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.StreamMaxClientDownloadSpeed, Value: \"-1\", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.StreamMaxClientUploadSpeed, Value: \"-1\", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.StreamMaxServerDownloadSpeed, Value: \"-1\", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t\t{Key: conf.StreamMaxServerUploadSpeed, Value: \"-1\", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},\n\t}\n\tadditionalSettingItems := tool.Tools.Items()\n\t// 固定顺序\n\tsort.Slice(additionalSettingItems, func(i, j int) bool {\n\t\treturn additionalSettingItems[i].Key < additionalSettingItems[j].Key\n\t})\n\tinitialSettingItems = append(initialSettingItems, additionalSettingItems...)\n\tif flags.Dev {\n\t\tinitialSettingItems = append(initialSettingItems, []model.SettingItem{\n\t\t\t{Key: \"test_deprecated\", Value: \"test_value\", Type: conf.TypeString, Flag: model.DEPRECATED},\n\t\t\t{Key: \"test_options\", Value: \"a\", Type: conf.TypeSelect, Options: \"a,b,c\"},\n\t\t\t{Key: \"test_help\", Type: conf.TypeString, Help: \"this is a help message\"},\n\t\t}...)\n\t}\n\treturn initialSettingItems\n}\n"
  },
  {
    "path": "internal/bootstrap/data/task.go",
    "content": "package data\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nvar initialTaskItems []model.TaskItem\n\nfunc initTasks() {\n\tInitialTasks()\n\n\tfor i := range initialTaskItems {\n\t\titem := &initialTaskItems[i]\n\t\ttaskitem, _ := db.GetTaskDataByType(item.Key)\n\t\tif taskitem == nil {\n\t\t\tdb.CreateTaskData(item)\n\t\t}\n\t}\n}\n\nfunc InitialTasks() []model.TaskItem {\n\tinitialTaskItems = []model.TaskItem{\n\t\t{Key: \"copy\", PersistData: \"[]\"},\n\t\t{Key: \"move\", PersistData: \"[]\"},\n\t\t{Key: \"download\", PersistData: \"[]\"},\n\t\t{Key: \"transfer\", PersistData: \"[]\"},\n\t}\n\treturn initialTaskItems\n}\n"
  },
  {
    "path": "internal/bootstrap/data/user.go",
    "content": "package data\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nfunc initUser() {\n\tadmin, err := op.GetAdmin()\n\tadminPassword := random.String(8)\n\tenvpass := os.Getenv(\"OPENLIST_ADMIN_PASSWORD\")\n\tif flags.Dev {\n\t\tadminPassword = \"admin\"\n\t} else if len(envpass) > 0 {\n\t\tadminPassword = envpass\n\t}\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tsalt := random.String(16)\n\t\t\tadmin = &model.User{\n\t\t\t\tUsername: \"admin\",\n\t\t\t\tSalt:     salt,\n\t\t\t\tPwdHash:  model.TwoHashPwd(adminPassword, salt),\n\t\t\t\tRole:     model.ADMIN,\n\t\t\t\tBasePath: \"/\",\n\t\t\t\tAuthn:    \"[]\",\n\t\t\t\t// 0(can see hidden) - 8(webdav read) & 12(can read archives) - 14(can share)\n\t\t\t\tPermission: 0x71FF,\n\t\t\t}\n\t\t\tif err := op.CreateUser(admin); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t} else {\n\t\t\t\t// DO NOT output the password to log file. Only output to console.\n\t\t\t\t// utils.Log.Infof(\"Successfully created the admin user and the initial password is: %s\", adminPassword)\n\t\t\t\tfmt.Printf(\"Successfully created the admin user and the initial password is: %s\\n\", adminPassword)\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Fatalf(\"[init user] Failed to get admin user: %v\", err)\n\t\t}\n\t}\n\t_, err = op.GetGuest()\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tsalt := random.String(16)\n\t\t\tguest := &model.User{\n\t\t\t\tUsername:   \"guest\",\n\t\t\t\tPwdHash:    model.TwoHashPwd(\"guest\", salt),\n\t\t\t\tSalt:       salt,\n\t\t\t\tRole:       model.GUEST,\n\t\t\t\tBasePath:   \"/\",\n\t\t\t\tPermission: 0,\n\t\t\t\tDisabled:   true,\n\t\t\t\tAuthn:      \"[]\",\n\t\t\t}\n\t\t\tif err := db.CreateUser(guest); err != nil {\n\t\t\t\tutils.Log.Fatalf(\"[init user] Failed to create guest user: %v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Fatalf(\"[init user] Failed to get guest user: %v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/db.go",
    "content": "package bootstrap\n\nimport (\n\t\"fmt\"\n\tstdlog \"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n\t\"gorm.io/gorm/schema\"\n)\n\nfunc InitDB() {\n\tlogLevel := logger.Silent\n\tif flags.Debug || flags.Dev {\n\t\tlogLevel = logger.Info\n\t}\n\tnewLogger := logger.New(\n\t\tstdlog.New(log.StandardLogger().Out, \"\\r\\n\", stdlog.LstdFlags),\n\t\tlogger.Config{\n\t\t\tSlowThreshold:             time.Second,\n\t\t\tLogLevel:                  logLevel,\n\t\t\tIgnoreRecordNotFoundError: true,\n\t\t\tColorful:                  true,\n\t\t},\n\t)\n\tgormConfig := &gorm.Config{\n\t\tNamingStrategy: schema.NamingStrategy{\n\t\t\tTablePrefix: conf.Conf.Database.TablePrefix,\n\t\t},\n\t\tLogger: newLogger,\n\t}\n\tvar dB *gorm.DB\n\tvar err error\n\tif flags.Dev {\n\t\tdB, err = gorm.Open(sqlite.Open(\"file::memory:?cache=shared\"), gormConfig)\n\t\tconf.Conf.Database.Type = \"sqlite3\"\n\t} else {\n\t\tdatabase := conf.Conf.Database\n\t\tswitch database.Type {\n\t\tcase \"sqlite3\":\n\t\t\t{\n\t\t\t\tif !(strings.HasSuffix(database.DBFile, \".db\") && len(database.DBFile) > 3) {\n\t\t\t\t\tlog.Fatalf(\"db name error.\")\n\t\t\t\t}\n\t\t\t\tdB, err = gorm.Open(sqlite.Open(fmt.Sprintf(\"%s?_journal=WAL&_vacuum=incremental\",\n\t\t\t\t\tdatabase.DBFile)), gormConfig)\n\t\t\t}\n\t\tcase \"mysql\":\n\t\t\t{\n\t\t\t\tdsn := database.DSN\n\t\t\t\tif dsn == \"\" {\n\t\t\t\t\t//[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]\n\t\t\t\t\tdsn = fmt.Sprintf(\"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&tls=%s\",\n\t\t\t\t\t\tdatabase.User, database.Password, database.Host, database.Port, database.Name, database.SSLMode)\n\t\t\t\t}\n\t\t\t\tdB, err = gorm.Open(mysql.Open(dsn), gormConfig)\n\t\t\t}\n\t\tcase \"postgres\":\n\t\t\t{\n\t\t\t\tdsn := database.DSN\n\t\t\t\tif dsn == \"\" {\n\t\t\t\t\tif database.Password != \"\" {\n\t\t\t\t\t\tdsn = fmt.Sprintf(\"host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai\",\n\t\t\t\t\t\t\tdatabase.Host, database.User, database.Password, database.Name, database.Port, database.SSLMode)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdsn = fmt.Sprintf(\"host=%s user=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai\",\n\t\t\t\t\t\t\tdatabase.Host, database.User, database.Name, database.Port, database.SSLMode)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdB, err = gorm.Open(postgres.Open(dsn), gormConfig)\n\t\t\t}\n\t\tdefault:\n\t\t\tlog.Fatalf(\"not supported database type: %s\", database.Type)\n\t\t}\n\t}\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to connect database:%s\", err.Error())\n\t}\n\tdb.Init(dB)\n}\n"
  },
  {
    "path": "internal/bootstrap/index.go",
    "content": "package bootstrap\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc InitIndex() {\n\tprogress, err := search.Progress()\n\tif err != nil {\n\t\tlog.Errorf(\"init index error: %+v\", err)\n\t\treturn\n\t}\n\tif !progress.IsDone {\n\t\tprogress.IsDone = true\n\t\tsearch.WriteProgress(progress)\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/log.go",
    "content": "package bootstrap\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/natefinch/lumberjack\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc init() {\n\tformatter := logrus.TextFormatter{\n\t\tForceColors:               true,\n\t\tEnvironmentOverrideColors: true,\n\t\tTimestampFormat:           \"2006-01-02 15:04:05\",\n\t\tFullTimestamp:             true,\n\t}\n\tlogrus.SetFormatter(&formatter)\n\tutils.Log.SetFormatter(&formatter)\n\t// logrus.SetLevel(logrus.DebugLevel)\n}\n\nfunc setLog(l *logrus.Logger) {\n\tif flags.Debug || flags.Dev {\n\t\tl.SetLevel(logrus.DebugLevel)\n\t\tl.SetReportCaller(true)\n\t} else {\n\t\tl.SetLevel(logrus.InfoLevel)\n\t\tl.SetReportCaller(false)\n\t}\n}\n\nfunc Log() {\n\tsetLog(logrus.StandardLogger())\n\tsetLog(utils.Log)\n\tlogConfig := conf.Conf.Log\n\tif logConfig.Enable {\n\t\tvar w io.Writer = &lumberjack.Logger{\n\t\t\tFilename:   logConfig.Name,\n\t\t\tMaxSize:    logConfig.MaxSize, // megabytes\n\t\t\tMaxBackups: logConfig.MaxBackups,\n\t\t\tMaxAge:     logConfig.MaxAge,   //days\n\t\t\tCompress:   logConfig.Compress, // disabled by default\n\t\t}\n\t\tif flags.Debug || flags.Dev || flags.LogStd {\n\t\t\tw = io.MultiWriter(os.Stdout, w)\n\t\t}\n\t\tlogrus.SetOutput(w)\n\t}\n\tlog.SetOutput(logrus.StandardLogger().Out)\n\tutils.Log.Infof(\"init logrus...\")\n\tutils.Log = logrus.StandardLogger()\n}\n"
  },
  {
    "path": "internal/bootstrap/offline_download.go",
    "content": "package bootstrap\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc InitOfflineDownloadTools() {\n\tfor k, v := range tool.Tools {\n\t\tres, err := v.Init()\n\t\tif err != nil {\n\t\t\tutils.Log.Warnf(\"init offline download tool %s failed: %s\", k, err)\n\t\t} else {\n\t\t\tutils.Log.Infof(\"init offline download tool %s success: %s\", k, res)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/all.go",
    "content": "package patch\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch/v3_24_0\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch/v3_32_0\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch/v3_41_0\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch/v4_1_8\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch/v4_1_9\"\n)\n\ntype VersionPatches struct {\n\t// Version means if the system is upgraded from Version or an earlier one\n\t// to the current version, all patches in Patches will be executed.\n\tVersion string\n\tPatches []func()\n}\n\nvar UpgradePatches = []VersionPatches{\n\t{\n\t\tVersion: \"v3.24.0\",\n\t\tPatches: []func(){\n\t\t\tv3_24_0.HashPwdForOldVersion,\n\t\t},\n\t},\n\t{\n\t\tVersion: \"v3.32.0\",\n\t\tPatches: []func(){\n\t\t\tv3_32_0.UpdateAuthnForOldVersion,\n\t\t},\n\t},\n\t{\n\t\tVersion: \"v3.41.0\",\n\t\tPatches: []func(){\n\t\t\tv3_41_0.GrantAdminPermissions,\n\t\t},\n\t},\n\t{\n\t\tVersion: \"v4.1.8\",\n\t\tPatches: []func(){\n\t\t\tv4_1_8.FixAliasConfig,\n\t\t},\n\t},\n\t{\n\t\tVersion: \"v4.1.9\",\n\t\tPatches: []func(){\n\t\t\tv4_1_9.EnableWebDavProxy,\n\t\t\tv4_1_9.ResetSkipTlsVerify,\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v3_24_0/hash_password.go",
    "content": "package v3_24_0\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// HashPwdForOldVersion encode passwords using SHA256\n// First published: 75acbcc perf: sha256 for user's password (close #3552) by Andy Hsu\nfunc HashPwdForOldVersion() {\n\tusers, _, err := op.GetUsers(1, -1)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[hash pwd for old version] failed get users: %v\", err)\n\t\treturn\n\t}\n\tfor i := range users {\n\t\tuser := users[i]\n\t\tif user.PwdHash == \"\" {\n\t\t\tuser.SetPassword(user.Password)\n\t\t\tuser.Password = \"\"\n\t\t\tif err := db.UpdateUser(&user); err != nil {\n\t\t\t\tutils.Log.Errorf(\"[hash pwd for old version] failed update user: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v3_32_0/update_authn.go",
    "content": "package v3_32_0\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// UpdateAuthnForOldVersion updates users' authn\n// First published: bdfc159 fix: webauthn logspam (#6181) by itsHenry\nfunc UpdateAuthnForOldVersion() {\n\tusers, _, err := op.GetUsers(1, -1)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[update authn for old version] failed get users: %v\", err)\n\t\treturn\n\t}\n\tfor i := range users {\n\t\tuser := users[i]\n\t\tif user.Authn == \"\" {\n\t\t\tuser.Authn = \"[]\"\n\t\t\tif err := db.UpdateUser(&user); err != nil {\n\t\t\t\tutils.Log.Errorf(\"[update authn for old version] failed update user: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v3_41_0/grant_permission.go",
    "content": "package v3_41_0\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// GrantAdminPermissions gives admin Permission 0(can see hidden) - 9(webdav manage) and\n// 12(can read archives) - 13(can decompress archives)\n// This patch is written to help users upgrading from older version better adapt\nfunc GrantAdminPermissions() {\n\tadmin, err := op.GetAdmin()\n\tif err == nil && (admin.Permission&0x33FF) == 0 {\n\t\tadmin.Permission |= 0x33FF\n\t\terr = op.UpdateUser(admin)\n\t}\n\tif err != nil {\n\t\tutils.Log.Errorf(\"Cannot grant permissions to admin: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v4_1_8/alias.go",
    "content": "package v4_1_8\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// FixAliasConfig upgrade the old version of the Addition of the Alias driver\nfunc FixAliasConfig() {\n\tstorages, _, err := db.GetStorages(1, -1)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[FixAliasConfig] failed to get storages: %s\", err.Error())\n\t\treturn\n\t}\n\tfor _, s := range storages {\n\t\tif s.Driver != \"Alias\" {\n\t\t\tcontinue\n\t\t}\n\t\taddition := make(map[string]any)\n\t\terr = utils.Json.UnmarshalFromString(s.Addition, &addition)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"[FixAliasConfig] failed to unmarshal addition of [%d]%s: %s\", s.ID, s.MountPath, err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := addition[\"read_conflict_policy\"]; ok {\n\t\t\tutils.Log.Infof(\"[FixAliasConfig] skip fixing [%d]%s because the addition already has \\\"read_conflict_policy\\\" key\", s.ID, s.MountPath)\n\t\t\tcontinue\n\t\t}\n\t\tvar protectSameName, parallelWrite, writable bool\n\t\tprotectSameNameAny, ok := addition[\"protect_same_name\"]\n\t\tif ok {\n\t\t\tdelete(addition, \"protect_same_name\")\n\t\t\tprotectSameName, ok = protectSameNameAny.(bool)\n\t\t}\n\t\tif !ok {\n\t\t\tprotectSameName = false\n\t\t}\n\t\tparallelWriteAny, ok := addition[\"parallel_write\"]\n\t\tif ok {\n\t\t\tdelete(addition, \"parallel_write\")\n\t\t\tparallelWrite, ok = parallelWriteAny.(bool)\n\t\t}\n\t\tif !ok {\n\t\t\tparallelWrite = false\n\t\t}\n\t\twritableAny, ok := addition[\"writable\"]\n\t\tif ok {\n\t\t\tdelete(addition, \"writable\")\n\t\t\twritable, ok = writableAny.(bool)\n\t\t}\n\t\tif !ok {\n\t\t\twritable = false\n\t\t}\n\t\tif !writable {\n\t\t\taddition[\"write_conflict_policy\"] = \"disabled\"\n\t\t\taddition[\"put_conflict_policy\"] = \"disabled\"\n\t\t} else if !protectSameName && !parallelWrite {\n\t\t\taddition[\"write_conflict_policy\"] = \"first\"\n\t\t\taddition[\"put_conflict_policy\"] = \"first\"\n\t\t} else if protectSameName && !parallelWrite {\n\t\t\taddition[\"write_conflict_policy\"] = \"deterministic\"\n\t\t\taddition[\"put_conflict_policy\"] = \"deterministic\"\n\t\t} else if !protectSameName && parallelWrite {\n\t\t\taddition[\"write_conflict_policy\"] = \"all\"\n\t\t\taddition[\"put_conflict_policy\"] = \"all\"\n\t\t} else {\n\t\t\taddition[\"write_conflict_policy\"] = \"deterministic_or_all\"\n\t\t\taddition[\"put_conflict_policy\"] = \"deterministic_or_all\"\n\t\t}\n\t\taddition[\"read_conflict_policy\"] = \"first\"\n\t\ts.Addition, err = utils.Json.MarshalToString(addition)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"[FixAliasConfig] failed to marshal addition of [%d]%s: %s\", s.ID, s.MountPath, err.Error())\n\t\t\tcontinue\n\t\t}\n\t\terr = db.UpdateStorage(&s)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"[FixAliasConfig] failed to update storage [%d]%s: %s\", s.ID, s.MountPath, err.Error())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v4_1_9/skip_tls.go",
    "content": "package v4_1_9\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc ResetSkipTlsVerify() {\n\tif !conf.Conf.TlsInsecureSkipVerify {\n\t\treturn\n\t}\n\tif !strings.HasPrefix(conf.Version, \"v\") {\n\t\treturn\n\t}\n\n\tconf.Conf.TlsInsecureSkipVerify = false\n\n\tconfBody, err := utils.Json.MarshalIndent(conf.Conf, \"\", \"  \")\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[ResetSkipTlsVerify] failed to rewrite config: marshal config error: %+v\", err)\n\t\treturn\n\t}\n\terr = os.WriteFile(conf.ConfigPath, confBody, 0o777)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[ResetSkipTlsVerify] failed to rewrite config: update config struct error: %+v\", err)\n\t\treturn\n\t}\n\tutils.Log.Infof(\"[ResetSkipTlsVerify] succeeded to set tls_insecure_skip_verify to false\")\n}\n"
  },
  {
    "path": "internal/bootstrap/patch/v4_1_9/webdav.go",
    "content": "package v4_1_9\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// EnableWebDavProxy updates Webdav driver storages to enable proxy\nfunc EnableWebDavProxy() {\n\tstorages, _, err := db.GetStorages(1, -1)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[EnableWebDavProxy] failed to get storages: %s\", err.Error())\n\t\treturn\n\t}\n\tfor _, s := range storages {\n\t\tif s.Driver != \"WebDav\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !s.WebProxy {\n\t\t\ts.WebProxy = true\n\t\t}\n\t\tif s.WebdavPolicy == \"302_redirect\" {\n\t\t\ts.WebdavPolicy = \"native_proxy\"\n\t\t}\n\t\terr = db.UpdateStorage(&s)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"[EnableWebDavProxy] failed to update storage [%d]%s: %s\", s.ID, s.MountPath, err.Error())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/patch.go",
    "content": "package bootstrap\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/patch\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nvar LastLaunchedVersion = \"\"\n\nfunc safeCall(v string, i int, f func()) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tutils.Log.Errorf(\"Recovered from patch (version: %s, index: %d) panic: %v\", v, i, r)\n\t\t}\n\t}()\n\n\tf()\n}\n\nfunc getVersion(v string) (major, minor, patchNum int, err error) {\n\t_, err = fmt.Sscanf(v, \"v%d.%d.%d\", &major, &minor, &patchNum)\n\treturn major, minor, patchNum, err\n}\n\nfunc compareVersion(majorA, minorA, patchNumA, majorB, minorB, patchNumB int) bool {\n\tif majorA != majorB {\n\t\treturn majorA > majorB\n\t}\n\tif minorA != minorB {\n\t\treturn minorA > minorB\n\t}\n\tif patchNumA != patchNumB {\n\t\treturn patchNumA > patchNumB\n\t}\n\treturn true\n}\n\nfunc InitUpgradePatch() {\n\tif !strings.HasPrefix(conf.Version, \"v\") {\n\t\tfor _, vp := range patch.UpgradePatches {\n\t\t\tfor i, p := range vp.Patches {\n\t\t\t\tsafeCall(vp.Version, i, p)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\tif LastLaunchedVersion == conf.Version {\n\t\treturn\n\t}\n\tif LastLaunchedVersion == \"\" {\n\t\tLastLaunchedVersion = \"v0.0.0\"\n\t}\n\tmajor, minor, patchNum, err := getVersion(LastLaunchedVersion)\n\tif err != nil {\n\t\tutils.Log.Warnf(\"Failed to parse last launched version %s: %v, skipping all patches and rewrite last launched version\", LastLaunchedVersion, err)\n\t\treturn\n\t}\n\tfor _, vp := range patch.UpgradePatches {\n\t\tma, mi, pn, err := getVersion(vp.Version)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"Skip invalid version %s patches: %v\", vp.Version, err)\n\t\t\tcontinue\n\t\t}\n\t\tif compareVersion(ma, mi, pn, major, minor, patchNum) {\n\t\t\tfor i, p := range vp.Patches {\n\t\t\t\tsafeCall(vp.Version, i, p)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/run.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/middlewares\"\n\t\"github.com/OpenListTeam/sftpd-openlist\"\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/quic-go/quic-go/http3\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n)\n\nfunc Init() {\n\tInitConfig()\n\tLog()\n\tInitDB()\n\tdata.InitData()\n\tInitStreamLimit()\n\tInitIndex()\n\tInitUpgradePatch()\n}\n\nfunc Release() {\n\tdb.Close()\n}\n\nvar (\n\trunning      bool\n\thttpSrv      *http.Server\n\thttpRunning  bool\n\thttpsSrv     *http.Server\n\thttpsRunning bool\n\tunixSrv      *http.Server\n\tunixRunning  bool\n\tquicSrv      *http3.Server\n\tquicRunning  bool\n\ts3Srv        *http.Server\n\ts3Running    bool\n\tftpDriver    *server.FtpMainDriver\n\tftpServer    *ftpserver.FtpServer\n\tftpRunning   bool\n\tsftpDriver   *server.SftpDriver\n\tsftpServer   *sftpd.SftpServer\n\tsftpRunning  bool\n)\n\n// Called by OpenList-Mobile\nfunc IsRunning(t string) bool {\n\tswitch t {\n\tcase \"http\":\n\t\treturn httpRunning\n\tcase \"https\":\n\t\treturn httpsRunning\n\tcase \"unix\":\n\t\treturn unixRunning\n\tcase \"quic\":\n\t\treturn quicRunning\n\tcase \"s3\":\n\t\treturn s3Running\n\tcase \"sftp\":\n\t\treturn sftpRunning\n\tcase \"ftp\":\n\t\treturn ftpRunning\n\t}\n\treturn running\n}\n\nfunc Start() {\n\tif conf.Conf.DelayedStart != 0 {\n\t\tutils.Log.Infof(\"delayed start for %d seconds\", conf.Conf.DelayedStart)\n\t\ttime.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)\n\t}\n\tInitOfflineDownloadTools()\n\tLoadStorages()\n\tInitTaskManager()\n\tif !flags.Debug && !flags.Dev {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t}\n\tr := gin.New()\n\n\t// gin log\n\tif conf.Conf.Log.Filter.Enable {\n\t\tr.Use(middlewares.FilteredLogger())\n\t} else {\n\t\tr.Use(gin.LoggerWithWriter(log.StandardLogger().Out))\n\t}\n\tr.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))\n\n\tserver.Init(r)\n\tvar httpHandler http.Handler = r\n\tif conf.Conf.Scheme.EnableH2c {\n\t\thttpHandler = h2c.NewHandler(r, &http2.Server{})\n\t}\n\tif conf.Conf.Scheme.HttpPort != -1 {\n\t\thttpBase := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)\n\t\tfmt.Printf(\"start HTTP server @ %s\\n\", httpBase)\n\t\tutils.Log.Infof(\"start HTTP server @ %s\", httpBase)\n\t\thttpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}\n\t\tgo func() {\n\t\t\thttpRunning = true\n\t\t\terr := httpSrv.ListenAndServe()\n\t\t\thttpRunning = false\n\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\thandleEndpointStartFailedHooks(\"http\", err)\n\t\t\t\tutils.Log.Errorf(\"failed to start http: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\thandleEndpointShutdownHooks(\"http\")\n\t\t\t}\n\t\t}()\n\t}\n\tif conf.Conf.Scheme.HttpsPort != -1 {\n\t\thttpsBase := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)\n\t\tfmt.Printf(\"start HTTPS server @ %s\\n\", httpsBase)\n\t\tutils.Log.Infof(\"start HTTPS server @ %s\", httpsBase)\n\t\thttpsSrv = &http.Server{Addr: httpsBase, Handler: r}\n\t\tgo func() {\n\t\t\thttpsRunning = true\n\t\t\terr := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)\n\t\t\thttpsRunning = false\n\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\thandleEndpointStartFailedHooks(\"https\", err)\n\t\t\t\tutils.Log.Errorf(\"failed to start https: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\thandleEndpointShutdownHooks(\"https\")\n\t\t\t}\n\t\t}()\n\t\tif conf.Conf.Scheme.EnableH3 {\n\t\t\tfmt.Printf(\"start HTTP3 (quic) server @ %s\\n\", httpsBase)\n\t\t\tutils.Log.Infof(\"start HTTP3 (quic) server @ %s\", httpsBase)\n\t\t\tr.Use(func(c *gin.Context) {\n\t\t\t\tif c.Request.TLS != nil {\n\t\t\t\t\tport := conf.Conf.Scheme.HttpsPort\n\t\t\t\t\tc.Header(\"Alt-Svc\", fmt.Sprintf(\"h3=\\\":%d\\\"; ma=86400\", port))\n\t\t\t\t}\n\t\t\t\tc.Next()\n\t\t\t})\n\t\t\tquicSrv = &http3.Server{Addr: httpsBase, Handler: r}\n\t\t\tgo func() {\n\t\t\t\tquicRunning = true\n\t\t\t\terr := quicSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)\n\t\t\t\tquicRunning = false\n\t\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\t\thandleEndpointStartFailedHooks(\"quic\", err)\n\t\t\t\t\tutils.Log.Errorf(\"failed to start http3 (quic): %s\", err.Error())\n\t\t\t\t} else {\n\t\t\t\t\thandleEndpointShutdownHooks(\"quic\")\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\tif conf.Conf.Scheme.UnixFile != \"\" {\n\t\tfmt.Printf(\"start unix server @ %s\\n\", conf.Conf.Scheme.UnixFile)\n\t\tutils.Log.Infof(\"start unix server @ %s\", conf.Conf.Scheme.UnixFile)\n\t\tunixSrv = &http.Server{Handler: httpHandler}\n\t\tgo func() {\n\t\t\tlistener, err := net.Listen(\"unix\", conf.Conf.Scheme.UnixFile)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"failed to listen unix: %+v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tunixRunning = true\n\t\t\t// set socket file permission\n\t\t\tmode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"failed to parse socket file permission: %+v\", err)\n\t\t\t} else {\n\t\t\t\terr = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.Log.Errorf(\"failed to chmod socket file: %+v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = unixSrv.Serve(listener)\n\t\t\tunixRunning = false\n\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\thandleEndpointStartFailedHooks(\"unix\", err)\n\t\t\t\tutils.Log.Errorf(\"failed to start unix: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\thandleEndpointShutdownHooks(\"unix\")\n\t\t\t}\n\t\t}()\n\t}\n\tif conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {\n\t\ts3r := gin.New()\n\t\ts3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))\n\t\tserver.InitS3(s3r)\n\t\ts3Base := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.S3.Port)\n\t\tfmt.Printf(\"start S3 server @ %s\\n\", s3Base)\n\t\tutils.Log.Infof(\"start S3 server @ %s\", s3Base)\n\t\tgo func() {\n\t\t\ts3Running = true\n\t\t\tvar err error\n\t\t\tif conf.Conf.S3.SSL {\n\t\t\t\ts3Srv = &http.Server{Addr: s3Base, Handler: s3r}\n\t\t\t\terr = s3Srv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)\n\t\t\t} else {\n\t\t\t\ts3Srv = &http.Server{Addr: s3Base, Handler: s3r}\n\t\t\t\terr = s3Srv.ListenAndServe()\n\t\t\t}\n\t\t\ts3Running = false\n\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\thandleEndpointStartFailedHooks(\"s3\", err)\n\t\t\t\tutils.Log.Errorf(\"failed to start s3 server: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\thandleEndpointShutdownHooks(\"s3\")\n\t\t\t}\n\t\t}()\n\t}\n\tif conf.Conf.FTP.Listen != \"\" && conf.Conf.FTP.Enable {\n\t\tvar err error\n\t\tftpDriver, err = server.NewMainDriver()\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to start ftp driver: %s\", err.Error())\n\t\t} else {\n\t\t\tfmt.Printf(\"start ftp server on %s\\n\", conf.Conf.FTP.Listen)\n\t\t\tutils.Log.Infof(\"start ftp server on %s\", conf.Conf.FTP.Listen)\n\t\t\tgo func() {\n\t\t\t\tftpServer = ftpserver.NewFtpServer(ftpDriver)\n\t\t\t\tftpRunning = true\n\t\t\t\terr = ftpServer.ListenAndServe()\n\t\t\t\tftpRunning = false\n\t\t\t\tif err != nil {\n\t\t\t\t\thandleEndpointStartFailedHooks(\"ftp\", err)\n\t\t\t\t\tutils.Log.Errorf(\"problem ftp server listening: %s\", err.Error())\n\t\t\t\t} else {\n\t\t\t\t\thandleEndpointShutdownHooks(\"ftp\")\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\tif conf.Conf.SFTP.Listen != \"\" && conf.Conf.SFTP.Enable {\n\t\tvar err error\n\t\tsftpDriver, err = server.NewSftpDriver()\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to start sftp driver: %s\", err.Error())\n\t\t} else {\n\t\t\tfmt.Printf(\"start sftp server on %s\", conf.Conf.SFTP.Listen)\n\t\t\tutils.Log.Infof(\"start sftp server on %s\", conf.Conf.SFTP.Listen)\n\t\t\tgo func() {\n\t\t\t\tsftpServer = sftpd.NewSftpServer(sftpDriver)\n\t\t\t\tsftpRunning = true\n\t\t\t\terr = sftpServer.RunServer()\n\t\t\t\tsftpRunning = false\n\t\t\t\tif err != nil {\n\t\t\t\t\thandleEndpointStartFailedHooks(\"sftp\", err)\n\t\t\t\t\tutils.Log.Errorf(\"problem sftp server listening: %s\", err.Error())\n\t\t\t\t} else {\n\t\t\t\t\thandleEndpointShutdownHooks(\"sftp\")\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\trunning = true\n}\n\nfunc Shutdown(timeout time.Duration) {\n\tutils.Log.Println(\"Shutdown server...\")\n\tfs.ArchiveContentUploadTaskManager.RemoveAll()\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\tvar wg sync.WaitGroup\n\tif httpSrv != nil && conf.Conf.Scheme.HttpPort != -1 {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := httpSrv.Shutdown(ctx); err != nil {\n\t\t\t\tutils.Log.Error(\"HTTP server shutdown err: \", err)\n\t\t\t}\n\t\t\thttpSrv = nil\n\t\t}()\n\t}\n\tif httpsSrv != nil && conf.Conf.Scheme.HttpsPort != -1 {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := httpsSrv.Shutdown(ctx); err != nil {\n\t\t\t\tutils.Log.Error(\"HTTPS server shutdown err: \", err)\n\t\t\t}\n\t\t\thttpsSrv = nil\n\t\t}()\n\t\tif quicSrv != nil && conf.Conf.Scheme.EnableH3 {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif err := quicSrv.Shutdown(ctx); err != nil {\n\t\t\t\t\tutils.Log.Error(\"HTTP3 (quic) server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t\tquicSrv = nil\n\t\t\t}()\n\t\t}\n\t}\n\tif unixSrv != nil && conf.Conf.Scheme.UnixFile != \"\" {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := unixSrv.Shutdown(ctx); err != nil {\n\t\t\t\tutils.Log.Error(\"Unix server shutdown err: \", err)\n\t\t\t}\n\t\t\tunixSrv = nil\n\t\t}()\n\t}\n\tif s3Srv != nil && conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := s3Srv.Shutdown(ctx); err != nil {\n\t\t\t\tutils.Log.Error(\"S3 server shutdown err: \", err)\n\t\t\t}\n\t\t\ts3Srv = nil\n\t\t}()\n\t}\n\tif conf.Conf.FTP.Listen != \"\" && conf.Conf.FTP.Enable && ftpServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif ftpDriver != nil {\n\t\t\t\tftpDriver.Stop()\n\t\t\t\tftpDriver = nil\n\t\t\t}\n\t\t\tif err := ftpServer.Stop(); err != nil {\n\t\t\t\tutils.Log.Error(\"FTP server shutdown err: \", err)\n\t\t\t}\n\t\t\tftpServer = nil\n\t\t}()\n\t}\n\tif conf.Conf.SFTP.Listen != \"\" && conf.Conf.SFTP.Enable && sftpServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := sftpServer.Close(); err != nil {\n\t\t\t\tutils.Log.Error(\"SFTP server shutdown err: \", err)\n\t\t\t}\n\t\t\tsftpServer = nil\n\t\t\tsftpDriver = nil\n\t\t}()\n\t}\n\twg.Wait()\n\tutils.Log.Println(\"Server exit\")\n\trunning = false\n}\n\ntype EndpointStartFailedHook func(string, string)\n\ntype EndpointShutdownHook func(string)\n\nvar (\n\tendpointStartFailedHooks map[string]EndpointStartFailedHook\n\tendpointShutdownHooks    map[string]EndpointShutdownHook\n)\n\nfunc RegisterEndpointStartFailedHook(hook EndpointStartFailedHook) string {\n\tid := uuid.NewString()\n\tendpointStartFailedHooks[id] = hook\n\treturn id\n}\n\nfunc RemoveEndpointStartFailedHook(id string) {\n\tdelete(endpointStartFailedHooks, id)\n}\n\nfunc RegisterEndpointShutdownHook(hook EndpointShutdownHook) string {\n\tid := uuid.NewString()\n\tendpointShutdownHooks[id] = hook\n\treturn id\n}\n\nfunc RemoveEndpointShutdownHook(id string) {\n\tdelete(endpointShutdownHooks, id)\n}\n\nfunc handleEndpointStartFailedHooks(t string, err error) {\n\tfor _, hook := range endpointStartFailedHooks {\n\t\thook(t, err.Error())\n\t}\n}\n\nfunc handleEndpointShutdownHooks(t string) {\n\tfor _, hook := range endpointShutdownHooks {\n\t\thook(t)\n\t}\n}\n\nfunc init() {\n\tendpointShutdownHooks = make(map[string]EndpointShutdownHook)\n\tendpointStartFailedHooks = make(map[string]EndpointStartFailedHook)\n}\n"
  },
  {
    "path": "internal/bootstrap/storage.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc LoadStorages() {\n\tstorages, err := db.GetEnabledStorages()\n\tif err != nil {\n\t\tutils.Log.Fatalf(\"failed get enabled storages: %+v\", err)\n\t}\n\tgo func(storages []model.Storage) {\n\t\tfor i := range storages {\n\t\t\terr := op.LoadStorage(context.Background(), storages[i])\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"failed get enabled storages: %+v\", err)\n\t\t\t} else {\n\t\t\t\tutils.Log.Infof(\"success load storage: [%s], driver: [%s], order: [%d]\",\n\t\t\t\t\tstorages[i].MountPath, storages[i].Driver, storages[i].Order)\n\t\t\t}\n\t\t}\n\t\tconf.SendStoragesLoadedSignal()\n\t}(storages)\n}\n"
  },
  {
    "path": "internal/bootstrap/stream_limit.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype blockBurstLimiter struct {\n\t*rate.Limiter\n}\n\nfunc (l blockBurstLimiter) WaitN(ctx context.Context, total int) error {\n\tfor total > 0 {\n\t\tn := l.Burst()\n\t\tif l.Limiter.Limit() == rate.Inf || n > total {\n\t\t\tn = total\n\t\t}\n\t\terr := l.Limiter.WaitN(ctx, n)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttotal -= n\n\t}\n\treturn nil\n}\n\nfunc streamFilterNegative(limit int) (rate.Limit, int) {\n\tif limit < 0 {\n\t\treturn rate.Inf, 0\n\t}\n\treturn rate.Limit(limit) * 1024.0, limit * 1024\n}\n\nfunc initLimiter(limiter *stream.Limiter, s string) {\n\tclientDownLimit, burst := streamFilterNegative(setting.GetInt(s, -1))\n\t*limiter = blockBurstLimiter{Limiter: rate.NewLimiter(clientDownLimit, burst)}\n\top.RegisterSettingChangingCallback(func() {\n\t\tnewLimit, newBurst := streamFilterNegative(setting.GetInt(s, -1))\n\t\t(*limiter).SetLimit(newLimit)\n\t\t(*limiter).SetBurst(newBurst)\n\t})\n}\n\nfunc InitStreamLimit() {\n\tinitLimiter(&stream.ClientDownloadLimit, conf.StreamMaxClientDownloadSpeed)\n\tinitLimiter(&stream.ClientUploadLimit, conf.StreamMaxClientUploadSpeed)\n\tinitLimiter(&stream.ServerDownloadLimit, conf.StreamMaxServerDownloadSpeed)\n\tinitLimiter(&stream.ServerUploadLimit, conf.StreamMaxServerUploadSpeed)\n}\n"
  },
  {
    "path": "internal/bootstrap/task.go",
    "content": "package bootstrap\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/tache\"\n)\n\nfunc taskFilterNegative(num int) int64 {\n\tif num < 0 {\n\t\tnum = 0\n\t}\n\treturn int64(num)\n}\n\nfunc InitTaskManager() {\n\tfs.UploadTaskManager = tache.NewManager[*fs.UploadTask](tache.WithWorks(setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers)), tache.WithMaxRetry(conf.Conf.Tasks.Upload.MaxRetry)) //upload will not support persist\n\top.RegisterSettingChangingCallback(func() {\n\t\tfs.UploadTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers)))\n\t})\n\tfs.CopyTaskManager = tache.NewManager[*fs.FileTransferTask](tache.WithWorks(setting.GetInt(conf.TaskCopyThreadsNum, conf.Conf.Tasks.Copy.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc(\"copy\", conf.Conf.Tasks.Copy.TaskPersistant), db.UpdateTaskDataFunc(\"copy\", conf.Conf.Tasks.Copy.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Copy.MaxRetry))\n\top.RegisterSettingChangingCallback(func() {\n\t\tfs.CopyTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskCopyThreadsNum, conf.Conf.Tasks.Copy.Workers)))\n\t})\n\tfs.MoveTaskManager = tache.NewManager[*fs.FileTransferTask](tache.WithWorks(setting.GetInt(conf.TaskMoveThreadsNum, conf.Conf.Tasks.Move.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc(\"move\", conf.Conf.Tasks.Move.TaskPersistant), db.UpdateTaskDataFunc(\"move\", conf.Conf.Tasks.Move.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Move.MaxRetry))\n\top.RegisterSettingChangingCallback(func() {\n\t\tfs.MoveTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskMoveThreadsNum, conf.Conf.Tasks.Move.Workers)))\n\t})\n\ttool.DownloadTaskManager = tache.NewManager[*tool.DownloadTask](tache.WithWorks(setting.GetInt(conf.TaskOfflineDownloadThreadsNum, conf.Conf.Tasks.Download.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc(\"download\", conf.Conf.Tasks.Download.TaskPersistant), db.UpdateTaskDataFunc(\"download\", conf.Conf.Tasks.Download.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Download.MaxRetry))\n\top.RegisterSettingChangingCallback(func() {\n\t\ttool.DownloadTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskOfflineDownloadThreadsNum, conf.Conf.Tasks.Download.Workers)))\n\t})\n\ttool.TransferTaskManager = tache.NewManager[*tool.TransferTask](tache.WithWorks(setting.GetInt(conf.TaskOfflineDownloadTransferThreadsNum, conf.Conf.Tasks.Transfer.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc(\"transfer\", conf.Conf.Tasks.Transfer.TaskPersistant), db.UpdateTaskDataFunc(\"transfer\", conf.Conf.Tasks.Transfer.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Transfer.MaxRetry))\n\top.RegisterSettingChangingCallback(func() {\n\t\ttool.TransferTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskOfflineDownloadTransferThreadsNum, conf.Conf.Tasks.Transfer.Workers)))\n\t})\n\tif len(tool.TransferTaskManager.GetAll()) == 0 { //prevent offline downloaded files from being deleted\n\t\tCleanTempDir()\n\t}\n\tfs.ArchiveDownloadTaskManager = tache.NewManager[*fs.ArchiveDownloadTask](tache.WithWorks(setting.GetInt(conf.TaskDecompressDownloadThreadsNum, conf.Conf.Tasks.Decompress.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc(\"decompress\", conf.Conf.Tasks.Decompress.TaskPersistant), db.UpdateTaskDataFunc(\"decompress\", conf.Conf.Tasks.Decompress.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Decompress.MaxRetry))\n\top.RegisterSettingChangingCallback(func() {\n\t\tfs.ArchiveDownloadTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskDecompressDownloadThreadsNum, conf.Conf.Tasks.Decompress.Workers)))\n\t})\n\tfs.ArchiveContentUploadTaskManager.Manager = tache.NewManager[*fs.ArchiveContentUploadTask](tache.WithWorks(setting.GetInt(conf.TaskDecompressUploadThreadsNum, conf.Conf.Tasks.DecompressUpload.Workers)), tache.WithMaxRetry(conf.Conf.Tasks.DecompressUpload.MaxRetry)) //decompress upload will not support persist\n\top.RegisterSettingChangingCallback(func() {\n\t\tfs.ArchiveContentUploadTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskDecompressUploadThreadsNum, conf.Conf.Tasks.DecompressUpload.Workers)))\n\t})\n}\n"
  },
  {
    "path": "internal/cache/keyed_cache.go",
    "content": "package cache\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\ntype KeyedCache[T any] struct {\n\tentries map[string]*CacheEntry[T]\n\tmu      sync.RWMutex\n\tttl     time.Duration\n}\n\nfunc NewKeyedCache[T any](ttl time.Duration) *KeyedCache[T] {\n\tc := &KeyedCache[T]{\n\t\tentries: make(map[string]*CacheEntry[T]),\n\t\tttl:     ttl,\n\t}\n\tgcFuncs = append(gcFuncs, c.GC)\n\treturn c\n}\n\nfunc (c *KeyedCache[T]) Set(key string, value T) {\n\tc.SetWithExpirable(key, value, ExpirationTime(time.Now().Add(c.ttl)))\n}\n\nfunc (c *KeyedCache[T]) SetWithTTL(key string, value T, ttl time.Duration) {\n\tc.SetWithExpirable(key, value, ExpirationTime(time.Now().Add(ttl)))\n}\n\nfunc (c *KeyedCache[T]) SetWithExpirable(key string, value T, exp Expirable) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.entries[key] = &CacheEntry[T]{\n\t\tdata:      value,\n\t\tExpirable: exp,\n\t}\n}\n\nfunc (c *KeyedCache[T]) Get(key string) (T, bool) {\n\tc.mu.RLock()\n\tentry, exists := c.entries[key]\n\tif !exists {\n\t\tc.mu.RUnlock()\n\t\treturn *new(T), false\n\t}\n\n\texpired := entry.Expired()\n\tc.mu.RUnlock()\n\n\tif !expired {\n\t\treturn entry.data, true\n\t}\n\n\tc.mu.Lock()\n\tif c.entries[key] == entry {\n\t\tdelete(c.entries, key)\n\t\tc.mu.Unlock()\n\t\treturn *new(T), false\n\t}\n\tc.mu.Unlock()\n\treturn *new(T), false\n}\n\nfunc (c *KeyedCache[T]) Delete(key string) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tdelete(c.entries, key)\n}\n\nfunc (c *KeyedCache[T]) Pop(key string) (T, bool) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif entry, exists := c.entries[key]; exists {\n\t\tdelete(c.entries, key)\n\t\treturn entry.data, true\n\t}\n\treturn *new(T), false\n}\n\nfunc (c *KeyedCache[T]) Clear() {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tc.entries = make(map[string]*CacheEntry[T])\n}\n\nfunc (c *KeyedCache[T]) GC() {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\texpiredKeys := make([]string, 0, len(c.entries))\n\tfor key, entry := range c.entries {\n\t\tif entry.Expired() {\n\t\t\texpiredKeys = append(expiredKeys, key)\n\t\t}\n\t}\n\tfor _, key := range expiredKeys {\n\t\tdelete(c.entries, key)\n\t}\n}\n"
  },
  {
    "path": "internal/cache/type.go",
    "content": "package cache\n\nimport \"time\"\n\ntype Expirable interface {\n\tExpired() bool\n}\n\ntype ExpirationTime time.Time\n\nfunc (e ExpirationTime) Expired() bool {\n\treturn time.Now().After(time.Time(e))\n}\n\ntype CacheEntry[T any] struct {\n\tExpirable\n\tdata T\n}\n"
  },
  {
    "path": "internal/cache/typed_cache.go",
    "content": "package cache\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\ntype TypedCache[T any] struct {\n\tentries map[string]map[string]*CacheEntry[T]\n\tmu      sync.RWMutex\n\tttl     time.Duration\n}\n\nfunc NewTypedCache[T any](ttl time.Duration) *TypedCache[T] {\n\tc := &TypedCache[T]{\n\t\tentries: make(map[string]map[string]*CacheEntry[T]),\n\t\tttl:     ttl,\n\t}\n\tgcFuncs = append(gcFuncs, c.GC)\n\treturn c\n}\n\nfunc (c *TypedCache[T]) SetType(key, typeKey string, value T) {\n\tc.SetTypeWithExpirable(key, typeKey, value, ExpirationTime(time.Now().Add(c.ttl)))\n}\n\nfunc (c *TypedCache[T]) SetTypeWithTTL(key, typeKey string, value T, ttl time.Duration) {\n\tc.SetTypeWithExpirable(key, typeKey, value, ExpirationTime(time.Now().Add(ttl)))\n}\n\nfunc (c *TypedCache[T]) SetTypeWithExpirable(key, typeKey string, value T, exp Expirable) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tcache, exists := c.entries[key]\n\tif !exists {\n\t\tcache = make(map[string]*CacheEntry[T])\n\t\tc.entries[key] = cache\n\t}\n\n\tcache[typeKey] = &CacheEntry[T]{\n\t\tdata:      value,\n\t\tExpirable: exp,\n\t}\n}\n\nfunc (c *TypedCache[T]) GetType(key, typeKey string) (T, bool) {\n\tc.mu.RLock()\n\tcache, exists := c.entries[key]\n\tif !exists {\n\t\tc.mu.RUnlock()\n\t\treturn *new(T), false\n\t}\n\tentry, exists := cache[typeKey]\n\tif !exists {\n\t\tc.mu.RUnlock()\n\t\treturn *new(T), false\n\t}\n\texpired := entry.Expired()\n\tc.mu.RUnlock()\n\n\tif !expired {\n\t\treturn entry.data, true\n\t}\n\n\tc.mu.Lock()\n\tif cache[typeKey] == entry {\n\t\tdelete(cache, typeKey)\n\t\tif len(cache) == 0 {\n\t\t\tdelete(c.entries, key)\n\t\t}\n\t\tc.mu.Unlock()\n\t\treturn *new(T), false\n\t}\n\tc.mu.Unlock()\n\treturn *new(T), false\n}\n\nfunc (c *TypedCache[T]) DeleteKey(key string) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tdelete(c.entries, key)\n}\n\nfunc (c *TypedCache[T]) Clear() {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tc.entries = make(map[string]map[string]*CacheEntry[T])\n}\n\nfunc (c *TypedCache[T]) GC() {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\texpiredKeys := make(map[string][]string)\n\tfor tk, entries := range c.entries {\n\t\tfor key, entry := range entries {\n\t\t\tif !entry.Expired() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := expiredKeys[tk]; !ok {\n\t\t\t\texpiredKeys[tk] = make([]string, 0, len(entries))\n\t\t\t}\n\t\t\texpiredKeys[tk] = append(expiredKeys[tk], key)\n\t\t}\n\t}\n\tfor tk, keys := range expiredKeys {\n\t\tfor _, key := range keys {\n\t\t\tdelete(c.entries[tk], key)\n\t\t}\n\t\tif len(c.entries[tk]) == 0 {\n\t\t\tdelete(c.entries, tk)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/cache/utils.go",
    "content": "package cache\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/cron\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tcacheGcCron *cron.Cron\n\tgcFuncs     []func()\n)\n\nfunc init() {\n\t// TODO Move to bootstrap\n\tcacheGcCron = cron.NewCron(time.Hour)\n\tcacheGcCron.Do(func() {\n\t\tlog.Infof(\"Start cache GC\")\n\t\tfor _, f := range gcFuncs {\n\t\t\tf()\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/conf/config.go",
    "content": "package conf\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n)\n\ntype Database struct {\n\tType        string `json:\"type\" env:\"TYPE\"`\n\tHost        string `json:\"host\" env:\"HOST\"`\n\tPort        int    `json:\"port\" env:\"PORT\"`\n\tUser        string `json:\"user\" env:\"USER\"`\n\tPassword    string `json:\"password\" env:\"PASS\"`\n\tName        string `json:\"name\" env:\"NAME\"`\n\tDBFile      string `json:\"db_file\" env:\"FILE\"`\n\tTablePrefix string `json:\"table_prefix\" env:\"TABLE_PREFIX\"`\n\tSSLMode     string `json:\"ssl_mode\" env:\"SSL_MODE\"`\n\tDSN         string `json:\"dsn\" env:\"DSN\"`\n}\n\ntype Meilisearch struct {\n\tHost   string `json:\"host\" env:\"HOST\"`\n\tAPIKey string `json:\"api_key\" env:\"API_KEY\"`\n\tIndex  string `json:\"index\" env:\"INDEX\"`\n}\n\ntype Scheme struct {\n\tAddress      string `json:\"address\" env:\"ADDR\"`\n\tHttpPort     int    `json:\"http_port\" env:\"HTTP_PORT\"`\n\tHttpsPort    int    `json:\"https_port\" env:\"HTTPS_PORT\"`\n\tForceHttps   bool   `json:\"force_https\" env:\"FORCE_HTTPS\"`\n\tCertFile     string `json:\"cert_file\" env:\"CERT_FILE\"`\n\tKeyFile      string `json:\"key_file\" env:\"KEY_FILE\"`\n\tUnixFile     string `json:\"unix_file\" env:\"UNIX_FILE\"`\n\tUnixFilePerm string `json:\"unix_file_perm\" env:\"UNIX_FILE_PERM\"`\n\tEnableH2c    bool   `json:\"enable_h2c\" env:\"ENABLE_H2C\"`\n\tEnableH3     bool   `json:\"enable_h3\" env:\"ENABLE_H3\"`\n}\n\ntype LogConfig struct {\n\tEnable     bool            `json:\"enable\" env:\"ENABLE\"`\n\tName       string          `json:\"name\" env:\"NAME\"`\n\tMaxSize    int             `json:\"max_size\" env:\"MAX_SIZE\"`\n\tMaxBackups int             `json:\"max_backups\" env:\"MAX_BACKUPS\"`\n\tMaxAge     int             `json:\"max_age\" env:\"MAX_AGE\"`\n\tCompress   bool            `json:\"compress\" env:\"COMPRESS\"`\n\tFilter     LogFilterConfig `json:\"filter\" envPrefix:\"FILTER_\"`\n}\n\ntype LogFilterConfig struct {\n\tEnable  bool     `json:\"enable\" env:\"ENABLE\"`\n\tFilters []Filter `json:\"filters\"`\n}\n\ntype Filter struct {\n\tCIDR   string `json:\"cidr\"`\n\tPath   string `json:\"path\"`\n\tMethod string `json:\"method\"`\n}\n\ntype TaskConfig struct {\n\tWorkers        int  `json:\"workers\" env:\"WORKERS\"`\n\tMaxRetry       int  `json:\"max_retry\" env:\"MAX_RETRY\"`\n\tTaskPersistant bool `json:\"task_persistant\" env:\"TASK_PERSISTANT\"`\n}\n\ntype TasksConfig struct {\n\tDownload           TaskConfig `json:\"download\" envPrefix:\"DOWNLOAD_\"`\n\tTransfer           TaskConfig `json:\"transfer\" envPrefix:\"TRANSFER_\"`\n\tUpload             TaskConfig `json:\"upload\" envPrefix:\"UPLOAD_\"`\n\tCopy               TaskConfig `json:\"copy\" envPrefix:\"COPY_\"`\n\tMove               TaskConfig `json:\"move\" envPrefix:\"MOVE_\"`\n\tDecompress         TaskConfig `json:\"decompress\" envPrefix:\"DECOMPRESS_\"`\n\tDecompressUpload   TaskConfig `json:\"decompress_upload\" envPrefix:\"DECOMPRESS_UPLOAD_\"`\n\tAllowRetryCanceled bool       `json:\"allow_retry_canceled\" env:\"ALLOW_RETRY_CANCELED\"`\n}\n\ntype Cors struct {\n\tAllowOrigins []string `json:\"allow_origins\" env:\"ALLOW_ORIGINS\"`\n\tAllowMethods []string `json:\"allow_methods\" env:\"ALLOW_METHODS\"`\n\tAllowHeaders []string `json:\"allow_headers\" env:\"ALLOW_HEADERS\"`\n}\n\ntype S3 struct {\n\tEnable bool `json:\"enable\" env:\"ENABLE\"`\n\tPort   int  `json:\"port\" env:\"PORT\"`\n\tSSL    bool `json:\"ssl\" env:\"SSL\"`\n}\n\ntype FTP struct {\n\tEnable                  bool   `json:\"enable\" env:\"ENABLE\"`\n\tListen                  string `json:\"listen\" env:\"LISTEN\"`\n\tFindPasvPortAttempts    int    `json:\"find_pasv_port_attempts\" env:\"FIND_PASV_PORT_ATTEMPTS\"`\n\tActiveTransferPortNon20 bool   `json:\"active_transfer_port_non_20\" env:\"ACTIVE_TRANSFER_PORT_NON_20\"`\n\tIdleTimeout             int    `json:\"idle_timeout\" env:\"IDLE_TIMEOUT\"`\n\tConnectionTimeout       int    `json:\"connection_timeout\" env:\"CONNECTION_TIMEOUT\"`\n\tDisableActiveMode       bool   `json:\"disable_active_mode\" env:\"DISABLE_ACTIVE_MODE\"`\n\tDefaultTransferBinary   bool   `json:\"default_transfer_binary\" env:\"DEFAULT_TRANSFER_BINARY\"`\n\tEnableActiveConnIPCheck bool   `json:\"enable_active_conn_ip_check\" env:\"ENABLE_ACTIVE_CONN_IP_CHECK\"`\n\tEnablePasvConnIPCheck   bool   `json:\"enable_pasv_conn_ip_check\" env:\"ENABLE_PASV_CONN_IP_CHECK\"`\n}\n\ntype SFTP struct {\n\tEnable bool   `json:\"enable\" env:\"ENABLE\"`\n\tListen string `json:\"listen\" env:\"LISTEN\"`\n}\n\ntype Config struct {\n\tForce                 bool        `json:\"force\" env:\"FORCE\"`\n\tSiteURL               string      `json:\"site_url\" env:\"SITE_URL\"`\n\tCdn                   string      `json:\"cdn\" env:\"CDN\"`\n\tJwtSecret             string      `json:\"jwt_secret\" env:\"JWT_SECRET\"`\n\tTokenExpiresIn        int         `json:\"token_expires_in\" env:\"TOKEN_EXPIRES_IN\"`\n\tDatabase              Database    `json:\"database\" envPrefix:\"DB_\"`\n\tMeilisearch           Meilisearch `json:\"meilisearch\" envPrefix:\"MEILISEARCH_\"`\n\tScheme                Scheme      `json:\"scheme\"`\n\tTempDir               string      `json:\"temp_dir\" env:\"TEMP_DIR\"`\n\tBleveDir              string      `json:\"bleve_dir\" env:\"BLEVE_DIR\"`\n\tDistDir               string      `json:\"dist_dir\"`\n\tLog                   LogConfig   `json:\"log\" envPrefix:\"LOG_\"`\n\tDelayedStart          int         `json:\"delayed_start\" env:\"DELAYED_START\"`\n\tMaxBufferLimit        int         `json:\"max_buffer_limitMB\" env:\"MAX_BUFFER_LIMIT_MB\"`\n\tMmapThreshold         int         `json:\"mmap_thresholdMB\" env:\"MMAP_THRESHOLD_MB\"`\n\tMaxConnections        int         `json:\"max_connections\" env:\"MAX_CONNECTIONS\"`\n\tMaxConcurrency        int         `json:\"max_concurrency\" env:\"MAX_CONCURRENCY\"`\n\tTlsInsecureSkipVerify bool        `json:\"tls_insecure_skip_verify\" env:\"TLS_INSECURE_SKIP_VERIFY\"`\n\tTasks                 TasksConfig `json:\"tasks\" envPrefix:\"TASKS_\"`\n\tCors                  Cors        `json:\"cors\" envPrefix:\"CORS_\"`\n\tS3                    S3          `json:\"s3\" envPrefix:\"S3_\"`\n\tFTP                   FTP         `json:\"ftp\" envPrefix:\"FTP_\"`\n\tSFTP                  SFTP        `json:\"sftp\" envPrefix:\"SFTP_\"`\n\tLastLaunchedVersion   string      `json:\"last_launched_version\"`\n\tProxyAddress          string      `json:\"proxy_address\" env:\"PROXY_ADDRESS\"`\n}\n\nfunc DefaultConfig(dataDir string) *Config {\n\ttempDir := filepath.Join(dataDir, \"temp\")\n\tindexDir := filepath.Join(dataDir, \"bleve\")\n\tlogPath := filepath.Join(dataDir, \"log/log.log\")\n\tdbPath := filepath.Join(dataDir, \"data.db\")\n\treturn &Config{\n\t\tScheme: Scheme{\n\t\t\tAddress:    \"0.0.0.0\",\n\t\t\tUnixFile:   \"\",\n\t\t\tHttpPort:   5244,\n\t\t\tHttpsPort:  -1,\n\t\t\tForceHttps: false,\n\t\t\tCertFile:   \"\",\n\t\t\tKeyFile:    \"\",\n\t\t},\n\t\tJwtSecret:      random.String(16),\n\t\tTokenExpiresIn: 48,\n\t\tTempDir:        tempDir,\n\t\tDatabase: Database{\n\t\t\tType:        \"sqlite3\",\n\t\t\tPort:        0,\n\t\t\tTablePrefix: \"x_\",\n\t\t\tDBFile:      dbPath,\n\t\t},\n\t\tMeilisearch: Meilisearch{\n\t\t\tHost:  \"http://localhost:7700\",\n\t\t\tIndex: \"openlist\",\n\t\t},\n\t\tBleveDir: indexDir,\n\t\tLog: LogConfig{\n\t\t\tEnable:     true,\n\t\t\tName:       logPath,\n\t\t\tMaxSize:    50,\n\t\t\tMaxBackups: 30,\n\t\t\tMaxAge:     28,\n\t\t\tFilter: LogFilterConfig{\n\t\t\t\tEnable: false,\n\t\t\t\tFilters: []Filter{\n\t\t\t\t\t{Path: \"/ping\"},\n\t\t\t\t\t{Method: \"HEAD\"},\n\t\t\t\t\t{Path: \"/dav/\", Method: \"PROPFIND\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMaxBufferLimit:        -1,\n\t\tMmapThreshold:         4,\n\t\tMaxConnections:        0,\n\t\tMaxConcurrency:        64,\n\t\tTlsInsecureSkipVerify: false,\n\t\tTasks: TasksConfig{\n\t\t\tDownload: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 1,\n\t\t\t\t// TaskPersistant: true,\n\t\t\t},\n\t\t\tTransfer: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 2,\n\t\t\t\t// TaskPersistant: true,\n\t\t\t},\n\t\t\tUpload: TaskConfig{\n\t\t\t\tWorkers: 5,\n\t\t\t},\n\t\t\tCopy: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 2,\n\t\t\t\t// TaskPersistant: true,\n\t\t\t},\n\t\t\tMove: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 2,\n\t\t\t\t// TaskPersistant: true,\n\t\t\t},\n\t\t\tDecompress: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 2,\n\t\t\t\t// TaskPersistant: true,\n\t\t\t},\n\t\t\tDecompressUpload: TaskConfig{\n\t\t\t\tWorkers:  5,\n\t\t\t\tMaxRetry: 2,\n\t\t\t},\n\t\t\tAllowRetryCanceled: false,\n\t\t},\n\t\tCors: Cors{\n\t\t\tAllowOrigins: []string{\"*\"},\n\t\t\tAllowMethods: []string{\"*\"},\n\t\t\tAllowHeaders: []string{\"*\"},\n\t\t},\n\t\tS3: S3{\n\t\t\tEnable: false,\n\t\t\tPort:   5246,\n\t\t\tSSL:    false,\n\t\t},\n\t\tFTP: FTP{\n\t\t\tEnable:                  false,\n\t\t\tListen:                  \":5221\",\n\t\t\tFindPasvPortAttempts:    50,\n\t\t\tActiveTransferPortNon20: false,\n\t\t\tIdleTimeout:             900,\n\t\t\tConnectionTimeout:       30,\n\t\t\tDisableActiveMode:       false,\n\t\t\tDefaultTransferBinary:   false,\n\t\t\tEnableActiveConnIPCheck: true,\n\t\t\tEnablePasvConnIPCheck:   true,\n\t\t},\n\t\tSFTP: SFTP{\n\t\t\tEnable: false,\n\t\t\tListen: \":5222\",\n\t\t},\n\t\tLastLaunchedVersion: \"\",\n\t\tProxyAddress:        \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/conf/const.go",
    "content": "package conf\n\nconst (\n\tTypeString = \"string\"\n\tTypeSelect = \"select\"\n\tTypeBool   = \"bool\"\n\tTypeText   = \"text\"\n\tTypeNumber = \"number\"\n)\n\nconst (\n\t// site\n\tVERSION      = \"version\"\n\tSiteTitle    = \"site_title\"\n\tAnnouncement = \"announcement\"\n\tAllowIndexed = \"allow_indexed\"\n\tAllowMounted = \"allow_mounted\"\n\tRobotsTxt    = \"robots_txt\"\n\n\tLogo                           = \"logo\" // multi-lines text, L1: light, EOL: dark\n\tFavicon                        = \"favicon\"\n\tMainColor                      = \"main_color\"\n\tHideStorageDetails             = \"hide_storage_details\"\n\tHideStorageDetailsInManagePage = \"hide_storage_details_in_manage_page\"\n\n\t// preview\n\tTextTypes                     = \"text_types\"\n\tAudioTypes                    = \"audio_types\"\n\tVideoTypes                    = \"video_types\"\n\tImageTypes                    = \"image_types\"\n\tProxyTypes                    = \"proxy_types\"\n\tProxyIgnoreHeaders            = \"proxy_ignore_headers\"\n\tAudioAutoplay                 = \"audio_autoplay\"\n\tVideoAutoplay                 = \"video_autoplay\"\n\tPreviewDownloadByDefault      = \"preview_download_by_default\"\n\tPreviewArchivesByDefault      = \"preview_archives_by_default\"\n\tSharePreviewDownloadByDefault = \"share_preview_download_by_default\"\n\tSharePreviewArchivesByDefault = \"share_preview_archives_by_default\"\n\tReadMeAutoRender              = \"readme_autorender\"\n\tFilterReadMeScripts           = \"filter_readme_scripts\"\n\tNonEFSZipEncoding             = \"non_efs_zip_encoding\"\n\n\t// global\n\tHideFiles               = \"hide_files\"\n\tCustomizeHead           = \"customize_head\"\n\tCustomizeBody           = \"customize_body\"\n\tLinkExpiration          = \"link_expiration\"\n\tSignAll                 = \"sign_all\"\n\tPrivacyRegs             = \"privacy_regs\"\n\tOcrApi                  = \"ocr_api\"\n\tFilenameCharMapping     = \"filename_char_mapping\"\n\tForwardDirectLinkParams = \"forward_direct_link_params\"\n\tIgnoreDirectLinkParams  = \"ignore_direct_link_params\"\n\tWebauthnLoginEnabled    = \"webauthn_login_enabled\"\n\tSharePreview            = \"share_preview\"\n\tShareArchivePreview     = \"share_archive_preview\"\n\tShareForceProxy         = \"share_force_proxy\"\n\tShareSummaryContent     = \"share_summary_content\"\n\tHandleHookAfterWriting  = \"handle_hook_after_writing\"\n\tHandleHookRateLimit     = \"handle_hook_rate_limit\"\n\tIgnoreSystemFiles       = \"ignore_system_files\"\n\n\t// index\n\tSearchIndex     = \"search_index\"\n\tAutoUpdateIndex = \"auto_update_index\"\n\tIgnorePaths     = \"ignore_paths\"\n\tMaxIndexDepth   = \"max_index_depth\"\n\n\t// aria2\n\tAria2Uri    = \"aria2_uri\"\n\tAria2Secret = \"aria2_secret\"\n\n\t// transmission\n\tTransmissionUri      = \"transmission_uri\"\n\tTransmissionSeedtime = \"transmission_seedtime\"\n\n\t// 115\n\tPan115TempDir = \"115_temp_dir\"\n\n\t// 123\n\tPan123TempDir = \"123_temp_dir\"\n\n\t// 115_open\n\tPan115OpenTempDir = \"115_open_temp_dir\"\n\n\t// pikpak\n\tPikPakTempDir = \"pikpak_temp_dir\"\n\n\t// thunder\n\tThunderTempDir = \"thunder_temp_dir\"\n\n\t// thunderx\n\tThunderXTempDir = \"thunderx_temp_dir\"\n\n\t// thunder_browser\n\tThunderBrowserTempDir = \"thunder_browser_temp_dir\"\n\n\t// single\n\tToken         = \"token\"\n\tIndexProgress = \"index_progress\"\n\n\t// SSO\n\tSSOClientId          = \"sso_client_id\"\n\tSSOClientSecret      = \"sso_client_secret\"\n\tSSOLoginEnabled      = \"sso_login_enabled\"\n\tSSOLoginPlatform     = \"sso_login_platform\"\n\tSSOOIDCUsernameKey   = \"sso_oidc_username_key\"\n\tSSOOrganizationName  = \"sso_organization_name\"\n\tSSOApplicationName   = \"sso_application_name\"\n\tSSOEndpointName      = \"sso_endpoint_name\"\n\tSSOJwtPublicKey      = \"sso_jwt_public_key\"\n\tSSOExtraScopes       = \"sso_extra_scopes\"\n\tSSOAutoRegister      = \"sso_auto_register\"\n\tSSODefaultDir        = \"sso_default_dir\"\n\tSSODefaultPermission = \"sso_default_permission\"\n\tSSOCompatibilityMode = \"sso_compatibility_mode\"\n\n\t// ldap\n\tLdapLoginEnabled      = \"ldap_login_enabled\"\n\tLdapServer            = \"ldap_server\"\n\tLdapSkipTlsVerify     = \"ldap_skip_tls_verify\"\n\tLdapManagerDN         = \"ldap_manager_dn\"\n\tLdapManagerPassword   = \"ldap_manager_password\"\n\tLdapUserSearchBase    = \"ldap_user_search_base\"\n\tLdapUserSearchFilter  = \"ldap_user_search_filter\"\n\tLdapDefaultPermission = \"ldap_default_permission\"\n\tLdapDefaultDir        = \"ldap_default_dir\"\n\tLdapLoginTips         = \"ldap_login_tips\"\n\n\t// s3\n\tS3Buckets         = \"s3_buckets\"\n\tS3AccessKeyId     = \"s3_access_key_id\"\n\tS3SecretAccessKey = \"s3_secret_access_key\"\n\n\t// qbittorrent\n\tQbittorrentUrl      = \"qbittorrent_url\"\n\tQbittorrentSeedtime = \"qbittorrent_seedtime\"\n\n\t// 123 open offline download\n\tPan123OpenOfflineDownloadCallbackUrl = \"123_open_callback_url\"\n\tPan123OpenTempDir                    = \"123_open_temp_dir\"\n\n\t// ftp\n\tFTPPublicHost            = \"ftp_public_host\"\n\tFTPPasvPortMap           = \"ftp_pasv_port_map\"\n\tFTPMandatoryTLS          = \"ftp_mandatory_tls\"\n\tFTPImplicitTLS           = \"ftp_implicit_tls\"\n\tFTPTLSPrivateKeyPath     = \"ftp_tls_private_key_path\"\n\tFTPTLSPublicCertPath     = \"ftp_tls_public_cert_path\"\n\tSFTPDisablePasswordLogin = \"sftp_disable_password_login\"\n\n\t// traffic\n\tTaskOfflineDownloadThreadsNum         = \"offline_download_task_threads_num\"\n\tTaskOfflineDownloadTransferThreadsNum = \"offline_download_transfer_task_threads_num\"\n\tTaskUploadThreadsNum                  = \"upload_task_threads_num\"\n\tTaskCopyThreadsNum                    = \"copy_task_threads_num\"\n\tTaskMoveThreadsNum                    = \"move_task_threads_num\"\n\tTaskDecompressDownloadThreadsNum      = \"decompress_download_task_threads_num\"\n\tTaskDecompressUploadThreadsNum        = \"decompress_upload_task_threads_num\"\n\tStreamMaxClientDownloadSpeed          = \"max_client_download_speed\"\n\tStreamMaxClientUploadSpeed            = \"max_client_upload_speed\"\n\tStreamMaxServerDownloadSpeed          = \"max_server_download_speed\"\n\tStreamMaxServerUploadSpeed            = \"max_server_upload_speed\"\n)\n\nconst (\n\tUNKNOWN = iota\n\tFOLDER\n\t// OFFICE\n\tVIDEO\n\tAUDIO\n\tTEXT\n\tIMAGE\n)\n\n// ContextKey is the type of context keys.\ntype ContextKey int8\n\nconst (\n\t_ ContextKey = iota\n\n\tNoTaskKey\n\tApiUrlKey\n\tUserKey\n\tMetaKey\n\tMetaPassKey\n\tClientIPKey\n\tProxyHeaderKey\n\tRequestHeaderKey\n\tUserAgentKey\n\tPathKey\n\tSharingIDKey\n\tSkipHookKey\n)\n"
  },
  {
    "path": "internal/conf/var.go",
    "content": "package conf\n\nimport (\n\t\"net/url\"\n\t\"regexp\"\n\t\"sync\"\n)\n\nvar (\n\tBuiltAt    string = \"unknown\"\n\tGitAuthor  string = \"unknown\"\n\tGitCommit  string = \"unknown\"\n\tVersion    string = \"dev\"\n\tWebVersion string = \"rolling\"\n)\n\nvar (\n\tConf       *Config\n\tURL        *url.URL\n\tConfigPath string\n)\n\nvar SlicesMap = make(map[string][]string)\nvar FilenameCharMap = make(map[string]string)\nvar PrivacyReg []*regexp.Regexp\n\nvar (\n\t// 单个Buffer最大限制\n\tMaxBufferLimit = 16 * 1024 * 1024\n\t// 超过该阈值的Buffer将使用 mmap 分配，可主动释放内存\n\tMmapThreshold = 4 * 1024 * 1024\n)\nvar (\n\tRawIndexHtml string\n\tManageHtml   string\n\tIndexHtml    string\n)\n\nvar (\n\t// StoragesLoaded loaded success if empty\n\tStoragesLoaded     = false\n\tstoragesLoadMu     sync.RWMutex\n\tstoragesLoadSignal chan struct{} = make(chan struct{})\n)\n\nfunc StoragesLoadSignal() <-chan struct{} {\n\tstoragesLoadMu.RLock()\n\tch := storagesLoadSignal\n\tstoragesLoadMu.RUnlock()\n\treturn ch\n}\nfunc SendStoragesLoadedSignal() {\n\tstoragesLoadMu.Lock()\n\tselect {\n\tcase <-storagesLoadSignal:\n\t\t// already closed\n\tdefault:\n\t\tStoragesLoaded = true\n\t\tclose(storagesLoadSignal)\n\t}\n\tstoragesLoadMu.Unlock()\n}\nfunc ResetStoragesLoadSignal() {\n\tstoragesLoadMu.Lock()\n\tselect {\n\tcase <-storagesLoadSignal:\n\t\tStoragesLoaded = false\n\t\tstoragesLoadSignal = make(chan struct{})\n\tdefault:\n\t\t// not closed -> nothing to do\n\t}\n\tstoragesLoadMu.Unlock()\n}\n"
  },
  {
    "path": "internal/db/db.go",
    "content": "package db\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"gorm.io/gorm\"\n)\n\nvar db *gorm.DB\n\nfunc Init(d *gorm.DB) {\n\tdb = d\n\terr := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))\n\tif err != nil {\n\t\tlog.Fatalf(\"failed migrate database: %s\", err.Error())\n\t}\n}\n\nfunc AutoMigrate(dst ...interface{}) error {\n\tvar err error\n\tif conf.Conf.Database.Type == \"mysql\" {\n\t\terr = db.Set(\"gorm:table_options\", \"ENGINE=InnoDB CHARSET=utf8mb4\").AutoMigrate(dst...)\n\t} else {\n\t\terr = db.AutoMigrate(dst...)\n\t}\n\treturn err\n}\n\nfunc GetDb() *gorm.DB {\n\treturn db\n}\n\nfunc Close() {\n\tlog.Info(\"closing db\")\n\tsqlDB, err := db.DB()\n\tif err != nil {\n\t\tlog.Errorf(\"failed to get db: %s\", err.Error())\n\t\treturn\n\t}\n\terr = sqlDB.Close()\n\tif err != nil {\n\t\tlog.Errorf(\"failed to close db: %s\", err.Error())\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/db/meta.go",
    "content": "package db\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetMetaByPath(path string) (*model.Meta, error) {\n\tmeta := model.Meta{Path: path}\n\tif err := db.Where(meta).First(&meta).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed select meta\")\n\t}\n\treturn &meta, nil\n}\n\nfunc GetMetaById(id uint) (*model.Meta, error) {\n\tvar u model.Meta\n\tif err := db.First(&u, id).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get old meta\")\n\t}\n\treturn &u, nil\n}\n\nfunc CreateMeta(u *model.Meta) error {\n\treturn errors.WithStack(db.Create(u).Error)\n}\n\nfunc UpdateMeta(u *model.Meta) error {\n\treturn errors.WithStack(db.Save(u).Error)\n}\n\nfunc GetMetas(pageIndex, pageSize int) (metas []model.Meta, count int64, err error) {\n\tmetaDB := db.Model(&model.Meta{})\n\tif err = metaDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get metas count\")\n\t}\n\tif err = metaDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&metas).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find metas\")\n\t}\n\treturn metas, count, nil\n}\n\nfunc DeleteMetaById(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.Meta{}, id).Error)\n}\n"
  },
  {
    "path": "internal/db/searchnode.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nfunc whereInParent(parent string) *gorm.DB {\n\tif parent == \"/\" {\n\t\treturn db.Where(\"1 = 1\")\n\t}\n\treturn db.Where(fmt.Sprintf(\"%s LIKE ?\", columnName(\"parent\")),\n\t\tfmt.Sprintf(\"%s/%%\", parent)).\n\t\tOr(fmt.Sprintf(\"%s = ?\", columnName(\"parent\")), parent)\n}\n\nfunc CreateSearchNode(node *model.SearchNode) error {\n\treturn db.Create(node).Error\n}\n\nfunc BatchCreateSearchNodes(nodes *[]model.SearchNode) error {\n\treturn db.CreateInBatches(nodes, 1000).Error\n}\n\nfunc DeleteSearchNodesByParent(path string) error {\n\tpath = utils.FixAndCleanPath(path)\n\terr := db.Where(whereInParent(path)).Delete(&model.SearchNode{}).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\tdir, name := stdpath.Split(path)\n\treturn db.Where(fmt.Sprintf(\"%s = ? AND %s = ?\",\n\t\tcolumnName(\"parent\"), columnName(\"name\")),\n\t\tdir, name).Delete(&model.SearchNode{}).Error\n}\n\nfunc ClearSearchNodes() error {\n\treturn db.Where(\"1 = 1\").Delete(&model.SearchNode{}).Error\n}\n\nfunc GetSearchNodesByParent(parent string) ([]model.SearchNode, error) {\n\tvar nodes []model.SearchNode\n\tif err := db.Where(fmt.Sprintf(\"%s = ?\",\n\t\tcolumnName(\"parent\")), parent).Find(&nodes).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn nodes, nil\n}\n\nfunc SearchNode(req model.SearchReq, useFullText bool) ([]model.SearchNode, int64, error) {\n\tvar searchDB *gorm.DB\n\tif !useFullText || conf.Conf.Database.Type == \"sqlite3\" {\n\t\tkeywordsClause := db.Where(\"1 = 1\")\n\t\tfor _, keyword := range strings.Fields(req.Keywords) {\n\t\t\tkeywordsClause = keywordsClause.Where(\"name LIKE ?\", fmt.Sprintf(\"%%%s%%\", keyword))\n\t\t}\n\t\tsearchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).Where(keywordsClause)\n\t} else {\n\t\tswitch conf.Conf.Database.Type {\n\t\tcase \"mysql\":\n\t\t\tsearchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).\n\t\t\t\tWhere(\"MATCH (name) AGAINST (? IN BOOLEAN MODE)\", \"'*\"+req.Keywords+\"*'\")\n\t\tcase \"postgres\":\n\t\t\tsearchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).\n\t\t\t\tWhere(\"to_tsvector(name) @@ to_tsquery(?)\", strings.Join(strings.Fields(req.Keywords), \" & \"))\n\t\t}\n\t}\n\n\tif req.Scope != 0 {\n\t\tisDir := req.Scope == 1\n\t\tsearchDB.Where(db.Where(\"is_dir = ?\", isDir))\n\t}\n\n\tvar count int64\n\tif err := searchDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get search items count\")\n\t}\n\tvar files []model.SearchNode\n\tif err := searchDB.Order(\"name asc\").Offset((req.Page - 1) * req.PerPage).Limit(req.PerPage).\n\t\tFind(&files).Error; err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn files, count, nil\n}\n"
  },
  {
    "path": "internal/db/settingitem.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetSettingItems() ([]model.SettingItem, error) {\n\tvar settingItems []model.SettingItem\n\tif err := db.Find(&settingItems).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn settingItems, nil\n}\n\nfunc GetSettingItemByKey(key string) (*model.SettingItem, error) {\n\tvar settingItem model.SettingItem\n\tif err := db.Where(fmt.Sprintf(\"%s = ?\", columnName(\"key\")), key).First(&settingItem).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn &settingItem, nil\n}\n\n// func GetSettingItemInKeys(keys []string) ([]model.SettingItem, error) {\n// \tvar settingItem []model.SettingItem\n// \tif err := db.Where(fmt.Sprintf(\"%s in ?\", columnName(\"key\")), keys).Find(&settingItem).Error; err != nil {\n// \t\treturn nil, errors.WithStack(err)\n// \t}\n// \treturn settingItem, nil\n// }\n\nfunc GetPublicSettingItems() ([]model.SettingItem, error) {\n\tvar settingItems []model.SettingItem\n\tif err := db.Where(fmt.Sprintf(\"%s in ?\", columnName(\"flag\")), []int{model.PUBLIC, model.READONLY}).Find(&settingItems).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn settingItems, nil\n}\n\nfunc GetSettingItemsByGroup(group int) ([]model.SettingItem, error) {\n\tvar settingItems []model.SettingItem\n\tif err := db.Where(fmt.Sprintf(\"%s = ?\", columnName(\"group\")), group).Find(&settingItems).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn settingItems, nil\n}\n\nfunc GetSettingItemsInGroups(groups []int) ([]model.SettingItem, error) {\n\tvar settingItems []model.SettingItem\n\terr := db.Order(columnName(\"index\")).Where(fmt.Sprintf(\"%s in ?\", columnName(\"group\")), groups).Find(&settingItems).Error\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn settingItems, nil\n}\n\nfunc SaveSettingItems(items []model.SettingItem) (err error) {\n\treturn errors.WithStack(db.Save(items).Error)\n}\n\nfunc SaveSettingItem(item *model.SettingItem) error {\n\treturn errors.WithStack(db.Save(item).Error)\n}\n\nfunc DeleteSettingItemByKey(key string) error {\n\treturn errors.WithStack(db.Delete(&model.SettingItem{Key: key}).Error)\n}\n"
  },
  {
    "path": "internal/db/sharing.go",
    "content": "package db\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetSharingById(id string) (*model.SharingDB, error) {\n\ts := model.SharingDB{ID: id}\n\tif err := db.Where(s).First(&s).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get sharing\")\n\t}\n\treturn &s, nil\n}\n\nfunc GetSharings(pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) {\n\tsharingDB := db.Model(&model.SharingDB{})\n\tif err := sharingDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get sharings count\")\n\t}\n\tif err := sharingDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&sharings).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find sharings\")\n\t}\n\treturn sharings, count, nil\n}\n\nfunc GetSharingsByCreatorId(creator uint, pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) {\n\tsharingDB := db.Model(&model.SharingDB{})\n\tcond := model.SharingDB{CreatorId: creator}\n\tif err := sharingDB.Where(cond).Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get sharings count\")\n\t}\n\tif err := sharingDB.Where(cond).Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&sharings).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find sharings\")\n\t}\n\treturn sharings, count, nil\n}\n\nfunc CreateSharing(s *model.SharingDB) (string, error) {\n\tif s.ID == \"\" {\n\t\tid := random.String(8)\n\t\tfor len(id) < 12 {\n\t\t\told := model.SharingDB{\n\t\t\t\tID: id,\n\t\t\t}\n\t\t\tif err := db.Where(old).First(&old).Error; err != nil {\n\t\t\t\ts.ID = id\n\t\t\t\treturn id, errors.WithStack(db.Create(s).Error)\n\t\t\t}\n\t\t\tid += random.String(1)\n\t\t}\n\t\treturn \"\", errors.New(\"failed find valid id\")\n\t} else {\n\t\tquery := model.SharingDB{ID: s.ID}\n\t\tif err := db.Where(query).First(&query).Error; err == nil {\n\t\t\treturn \"\", errors.New(\"sharing already exist\")\n\t\t}\n\t\treturn s.ID, errors.WithStack(db.Create(s).Error)\n\t}\n}\n\nfunc UpdateSharing(s *model.SharingDB) error {\n\treturn errors.WithStack(db.Save(s).Error)\n}\n\nfunc DeleteSharingById(id string) error {\n\ts := model.SharingDB{ID: id}\n\treturn errors.WithStack(db.Where(s).Delete(&s).Error)\n}\n\nfunc DeleteSharingsByCreatorId(creatorId uint) error {\n\treturn errors.WithStack(db.Where(\"creator_id = ?\", creatorId).Delete(&model.SharingDB{}).Error)\n}\n"
  },
  {
    "path": "internal/db/sshkey.go",
    "content": "package db\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetSSHPublicKeyByUserId(userId uint, pageIndex, pageSize int) (keys []model.SSHPublicKey, count int64, err error) {\n\tkeyDB := db.Model(&model.SSHPublicKey{})\n\tquery := model.SSHPublicKey{UserId: userId}\n\tif err := keyDB.Where(query).Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get user's keys count\")\n\t}\n\tif err := keyDB.Where(query).Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&keys).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find user's keys\")\n\t}\n\treturn keys, count, nil\n}\n\nfunc GetSSHPublicKeyById(id uint) (*model.SSHPublicKey, error) {\n\tvar k model.SSHPublicKey\n\tif err := db.First(&k, id).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get old key\")\n\t}\n\treturn &k, nil\n}\n\nfunc GetSSHPublicKeyByUserTitle(userId uint, title string) (*model.SSHPublicKey, error) {\n\tkey := model.SSHPublicKey{UserId: userId, Title: title}\n\tif err := db.Where(key).First(&key).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed find key with title of user\")\n\t}\n\treturn &key, nil\n}\n\nfunc CreateSSHPublicKey(k *model.SSHPublicKey) error {\n\treturn errors.WithStack(db.Create(k).Error)\n}\n\nfunc UpdateSSHPublicKey(k *model.SSHPublicKey) error {\n\treturn errors.WithStack(db.Save(k).Error)\n}\n\nfunc GetSSHPublicKeys(pageIndex, pageSize int) (keys []model.SSHPublicKey, count int64, err error) {\n\tkeyDB := db.Model(&model.SSHPublicKey{})\n\tif err := keyDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get keys count\")\n\t}\n\tif err := keyDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&keys).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find keys\")\n\t}\n\treturn keys, count, nil\n}\n\nfunc DeleteSSHPublicKeyById(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.SSHPublicKey{}, id).Error)\n}\n"
  },
  {
    "path": "internal/db/storage.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n)\n\n// why don't need `cache` for storage?\n// because all storage store in `op.storagesMap`\n// the most of the read operation is from `op.storagesMap`\n// just for persistence in database\n\n// CreateStorage just insert storage to database\nfunc CreateStorage(storage *model.Storage) error {\n\treturn errors.WithStack(db.Create(storage).Error)\n}\n\n// UpdateStorage just update storage in database\nfunc UpdateStorage(storage *model.Storage) error {\n\treturn errors.WithStack(db.Save(storage).Error)\n}\n\n// DeleteStorageById just delete storage from database by id\nfunc DeleteStorageById(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.Storage{}, id).Error)\n}\n\n// GetStorages Get all storages from database order by index\nfunc GetStorages(pageIndex, pageSize int) ([]model.Storage, int64, error) {\n\tstorageDB := db.Model(&model.Storage{})\n\tvar count int64\n\tif err := storageDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get storages count\")\n\t}\n\tvar storages []model.Storage\n\tif err := addStorageOrder(storageDB).Order(columnName(\"order\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&storages).Error; err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\treturn storages, count, nil\n}\n\n// GetStorageById Get Storage by id, used to update storage usually\nfunc GetStorageById(id uint) (*model.Storage, error) {\n\tvar storage model.Storage\n\tstorage.ID = id\n\tif err := db.First(&storage).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn &storage, nil\n}\n\n// GetStorageByMountPath Get Storage by mountPath, used to update storage usually\nfunc GetStorageByMountPath(mountPath string) (*model.Storage, error) {\n\tvar storage model.Storage\n\tif err := db.Where(\"mount_path = ?\", mountPath).First(&storage).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn &storage, nil\n}\n\nfunc GetEnabledStorages() ([]model.Storage, error) {\n\tvar storages []model.Storage\n\terr := addStorageOrder(db).Where(fmt.Sprintf(\"%s = ?\", columnName(\"disabled\")), false).Find(&storages).Error\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn storages, nil\n}\n"
  },
  {
    "path": "internal/db/tasks.go",
    "content": "package db\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetTaskDataByType(type_s string) (*model.TaskItem, error) {\n\ttask := model.TaskItem{Key: type_s}\n\tif err := db.Where(task).First(&task).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed find task\")\n\t}\n\treturn &task, nil\n}\n\nfunc UpdateTaskData(t *model.TaskItem) error {\n\treturn errors.WithStack(db.Model(&model.TaskItem{}).Where(\"key = ?\", t.Key).Update(\"persist_data\", t.PersistData).Error)\n}\n\nfunc CreateTaskData(t *model.TaskItem) error {\n\treturn errors.WithStack(db.Create(t).Error)\n}\n\nfunc GetTaskDataFunc(type_s string, enabled bool) func() ([]byte, error) {\n\tif !enabled {\n\t\treturn nil\n\t}\n\ttask, err := GetTaskDataByType(type_s)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn func() ([]byte, error) {\n\t\t<-conf.StoragesLoadSignal()\n\t\treturn []byte(task.PersistData), nil\n\t}\n}\n\nfunc UpdateTaskDataFunc(type_s string, enabled bool) func([]byte) error {\n\tif !enabled {\n\t\treturn nil\n\t}\n\treturn func(data []byte) error {\n\t\ts := string(data)\n\t\tif s == \"null\" || s == \"\" {\n\t\t\ts = \"[]\"\n\t\t}\n\t\treturn UpdateTaskData(&model.TaskItem{Key: type_s, PersistData: s})\n\t}\n}\n"
  },
  {
    "path": "internal/db/user.go",
    "content": "package db\n\nimport (\n\t\"encoding/base64\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc GetUserByRole(role int) (*model.User, error) {\n\tuser := model.User{Role: role}\n\tif err := db.Where(user).Take(&user).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn &user, nil\n}\n\nfunc GetUserByName(username string) (*model.User, error) {\n\tuser := model.User{Username: username}\n\tif err := db.Where(user).First(&user).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed find user\")\n\t}\n\treturn &user, nil\n}\n\nfunc GetUserBySSOID(ssoID string) (*model.User, error) {\n\tuser := model.User{SsoID: ssoID}\n\tif err := db.Where(user).First(&user).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"The single sign on platform is not bound to any users\")\n\t}\n\treturn &user, nil\n}\n\nfunc GetUserById(id uint) (*model.User, error) {\n\tvar u model.User\n\tif err := db.First(&u, id).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get old user\")\n\t}\n\treturn &u, nil\n}\n\nfunc CreateUser(u *model.User) error {\n\treturn errors.WithStack(db.Create(u).Error)\n}\n\nfunc UpdateUser(u *model.User) error {\n\treturn errors.WithStack(db.Save(u).Error)\n}\n\nfunc GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err error) {\n\tuserDB := db.Model(&model.User{})\n\tif err := userDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get users count\")\n\t}\n\tif err := userDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&users).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find users\")\n\t}\n\treturn users, count, nil\n}\n\nfunc DeleteUserById(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.User{}, id).Error)\n}\n\nfunc UpdateAuthn(userID uint, authn string) error {\n\treturn db.Model(&model.User{ID: userID}).Update(\"authn\", authn).Error\n}\n\nfunc RegisterAuthn(u *model.User, credential *webauthn.Credential) error {\n\tif u == nil {\n\t\treturn errors.New(\"user is nil\")\n\t}\n\texists := u.WebAuthnCredentials()\n\tif credential != nil {\n\t\texists = append(exists, *credential)\n\t}\n\tres, err := utils.Json.Marshal(exists)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn UpdateAuthn(u.ID, string(res))\n}\n\nfunc RemoveAuthn(u *model.User, id string) error {\n\texists := u.WebAuthnCredentials()\n\tfor i := 0; i < len(exists); i++ {\n\t\tidEncoded := base64.StdEncoding.EncodeToString(exists[i].ID)\n\t\tif idEncoded == id {\n\t\t\texists[len(exists)-1], exists[i] = exists[i], exists[len(exists)-1]\n\t\t\texists = exists[:len(exists)-1]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tres, err := utils.Json.Marshal(exists)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn UpdateAuthn(u.ID, string(res))\n}\n"
  },
  {
    "path": "internal/db/util.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"gorm.io/gorm\"\n)\n\nfunc columnName(name string) string {\n\tif conf.Conf.Database.Type == \"postgres\" {\n\t\treturn fmt.Sprintf(`\"%s\"`, name)\n\t}\n\treturn fmt.Sprintf(\"`%s`\", name)\n}\n\nfunc addStorageOrder(db *gorm.DB) *gorm.DB {\n\treturn db.Order(fmt.Sprintf(\"%s, %s\", columnName(\"order\"), columnName(\"id\")))\n}\n"
  },
  {
    "path": "internal/driver/config.go",
    "content": "package driver\n\ntype Config struct {\n\tName      string `json:\"name\"`\n\tLocalSort bool   `json:\"local_sort\"`\n\tOnlyProxy bool   `json:\"only_proxy\"`\n\tNoCache   bool   `json:\"no_cache\"`\n\tNoUpload  bool   `json:\"no_upload\"`\n\t// if need get message from user, such as validate code\n\tNeedMs      bool   `json:\"need_ms\"`\n\tDefaultRoot string `json:\"default_root\"`\n\tCheckStatus bool   `json:\"-\"`\n\t//info,success,warning,danger\n\tAlert string `json:\"alert\"`\n\t// whether to support overwrite upload\n\tNoOverwriteUpload bool `json:\"-\"`\n\tProxyRangeOption  bool `json:\"-\"`\n\t// if the driver returns Link without URL, this should be set to true\n\tNoLinkURL bool `json:\"-\"`\n\t// Link cache behaviour:\n\t//  - LinkCacheAuto: let driver decide per-path (implement driver.LinkCacheModeResolver)\n\t//  - LinkCacheNone: no extra info added to cache key (default)\n\t//  - flags (OR-able) can add more attributes to cache key (IP, UA, ...)\n\tLinkCacheMode `json:\"-\"`\n\t// if the driver only store indices of files (e.g. UrlTree)\n\tOnlyIndices bool `json:\"only_indices\"`\n\t// prefer proxy download even if direct link is available\n\tPreferProxy bool `json:\"prefer_proxy\"`\n}\ntype LinkCacheMode int8\n\nconst (\n\tLinkCacheAuto LinkCacheMode = -1 // Let the driver decide per-path (use driver.LinkCacheModeResolver)\n\tLinkCacheNone LinkCacheMode = 0  // No extra info added to cache key (default)\n)\n\nconst (\n\tLinkCacheIP LinkCacheMode = 1 << iota // include client IP in cache key\n\tLinkCacheUA                           // include User-Agent in cache key\n)\n\nfunc (c Config) MustProxy() bool {\n\treturn c.OnlyProxy || c.NoLinkURL\n}\n\nfunc (c Config) DefaultProxy() bool {\n\treturn c.PreferProxy\n}\n"
  },
  {
    "path": "internal/driver/driver.go",
    "content": "package driver\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Driver interface {\n\tMeta\n\tReader\n\t// Writer\n\t// Other\n}\n\ntype Meta interface {\n\tConfig() Config\n\t// GetStorage just get raw storage, no need to implement, because model.Storage have implemented\n\tGetStorage() *model.Storage\n\tSetStorage(model.Storage)\n\t// GetAddition Additional is used for unmarshal of JSON, so need return pointer\n\tGetAddition() Additional\n\t// Init If already initialized, drop first\n\tInit(ctx context.Context) error\n\tDrop(ctx context.Context) error\n}\n\ntype Other interface {\n\tOther(ctx context.Context, args model.OtherArgs) (interface{}, error)\n}\n\ntype Reader interface {\n\t// List files in the path\n\t// if identify files by path, need to set ID with path,like path.Join(dir.GetID(), obj.GetName())\n\t// if identify files by id, need to set ID with corresponding id\n\tList(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error)\n\t// Link get url/filepath/reader of file\n\tLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error)\n}\n\ntype GetRooter interface {\n\tGetRoot(ctx context.Context) (model.Obj, error)\n}\n\ntype Getter interface {\n\t// Get file by path, the path haven't been joined with root path\n\tGet(ctx context.Context, path string) (model.Obj, error)\n}\n\n//type Writer interface {\n//\tMkdir\n//\tMove\n//\tRename\n//\tCopy\n//\tRemove\n//\tPut\n//}\n\ntype Mkdir interface {\n\tMakeDir(ctx context.Context, parentDir model.Obj, dirName string) error\n}\n\ntype Move interface {\n\tMove(ctx context.Context, srcObj, dstDir model.Obj) error\n}\n\ntype Rename interface {\n\tRename(ctx context.Context, srcObj model.Obj, newName string) error\n}\n\ntype Copy interface {\n\tCopy(ctx context.Context, srcObj, dstDir model.Obj) error\n}\n\ntype Remove interface {\n\tRemove(ctx context.Context, obj model.Obj) error\n}\n\ntype Put interface {\n\t// Put a file (provided as a FileStreamer) into the driver\n\t// Besides the most basic upload functionality, the following features also need to be implemented:\n\t// 1. Canceling (when `<-ctx.Done()` returns), which can be supported by the following methods:\n\t//   (1) Use request methods that carry context, such as the following:\n\t//      a. http.NewRequestWithContext\n\t//      b. resty.Request.SetContext\n\t//      c. s3manager.Uploader.UploadWithContext\n\t//      d. utils.CopyWithCtx\n\t//   (2) Use a `driver.ReaderWithCtx` or `driver.NewLimitedUploadStream`\n\t//   (3) Use `utils.IsCanceled` to check if the upload has been canceled during the upload process,\n\t//       this is typically applicable to chunked uploads.\n\t// 2. Submit upload progress (via `up`) in real-time. There are three recommended ways as follows:\n\t//   (1) Use `utils.CopyWithCtx`\n\t//   (2) Use `driver.ReaderUpdatingProgress`\n\t//   (3) Use `driver.Progress` with `io.TeeReader`\n\t// 3. Slow down upload speed (via `stream.ServerUploadLimit`). It requires you to wrap the read stream\n\t//    in a `driver.RateLimitReader` or a `driver.RateLimitFile` after calculating the file's hash and\n\t//    before uploading the file or file chunks. Or you can directly call `driver.ServerUploadLimitWaitN`\n\t//    if your file chunks are sufficiently small (less than about 50KB).\n\t// NOTE that the network speed may be significantly slower than the stream's read speed. Therefore, if\n\t// you use a `errgroup.Group` to upload each chunk in parallel, you should use `Group.SetLimit` to\n\t// limit the maximum number of upload threads, preventing excessive memory usage caused by buffering\n\t// too many file chunks awaiting upload.\n\tPut(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up UpdateProgress) error\n}\n\ntype PutURL interface {\n\t// PutURL directly put a URL into the storage\n\t// Applicable to index-based drivers like URL-Tree or drivers that support uploading files as URLs\n\t// Called when using SimpleHttp for offline downloading, skipping creating a download task\n\tPutURL(ctx context.Context, dstDir model.Obj, name, url string) error\n}\n\ntype MkdirResult interface {\n\tMakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error)\n}\n\ntype MoveResult interface {\n\tMove(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error)\n}\n\ntype RenameResult interface {\n\tRename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error)\n}\n\ntype CopyResult interface {\n\tCopy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error)\n}\n\ntype PutResult interface {\n\t// Put a file (provided as a FileStreamer) into the driver and return the put obj\n\t// Besides the most basic upload functionality, the following features also need to be implemented:\n\t// 1. Canceling (when `<-ctx.Done()` returns), which can be supported by the following methods:\n\t//   (1) Use request methods that carry context, such as the following:\n\t//      a. http.NewRequestWithContext\n\t//      b. resty.Request.SetContext\n\t//      c. s3manager.Uploader.UploadWithContext\n\t//      d. utils.CopyWithCtx\n\t//   (2) Use a `driver.ReaderWithCtx` or `driver.NewLimitedUploadStream`\n\t//   (3) Use `utils.IsCanceled` to check if the upload has been canceled during the upload process,\n\t//       this is typically applicable to chunked uploads.\n\t// 2. Submit upload progress (via `up`) in real-time. There are three recommended ways as follows:\n\t//   (1) Use `utils.CopyWithCtx`\n\t//   (2) Use `driver.ReaderUpdatingProgress`\n\t//   (3) Use `driver.Progress` with `io.TeeReader`\n\t// 3. Slow down upload speed (via `stream.ServerUploadLimit`). It requires you to wrap the read stream\n\t//    in a `driver.RateLimitReader` or a `driver.RateLimitFile` after calculating the file's hash and\n\t//    before uploading the file or file chunks. Or you can directly call `driver.ServerUploadLimitWaitN`\n\t//    if your file chunks are sufficiently small (less than about 50KB).\n\t// NOTE that the network speed may be significantly slower than the stream's read speed. Therefore, if\n\t// you use a `errgroup.Group` to upload each chunk in parallel, you should use `Group.SetLimit` to\n\t// limit the maximum number of upload threads, preventing excessive memory usage caused by buffering\n\t// too many file chunks awaiting upload.\n\tPut(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up UpdateProgress) (model.Obj, error)\n}\n\ntype PutURLResult interface {\n\t// PutURL directly put a URL into the storage\n\t// Applicable to index-based drivers like URL-Tree or drivers that support uploading files as URLs\n\t// Called when using SimpleHttp for offline downloading, skipping creating a download task\n\tPutURL(ctx context.Context, dstDir model.Obj, name, url string) (model.Obj, error)\n}\n\ntype ArchiveReader interface {\n\t// GetArchiveMeta get the meta-info of an archive\n\t// return errs.WrongArchivePassword if the meta-info is also encrypted but provided password is wrong or empty\n\t// return errs.NotImplement to use internal archive tools to get the meta-info, such as the following cases:\n\t// 1. the driver do not support the format of the archive but there may be an internal tool do\n\t// 2. handling archives is a VIP feature, but the driver does not have VIP access\n\tGetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error)\n\t// ListArchive list the children of model.ArchiveArgs.InnerPath in the archive\n\t// return errs.NotImplement to use internal archive tools to list the children\n\t// return errs.NotSupport if the folder structure should be acquired from model.ArchiveMeta.GetTree\n\tListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error)\n\t// Extract get url/filepath/reader of a file in the archive\n\t// return errs.NotImplement to use internal archive tools to extract\n\tExtract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error)\n}\n\ntype ArchiveGetter interface {\n\t// ArchiveGet get file by inner path\n\t// return errs.NotImplement to use internal archive tools to get the children\n\t// return errs.NotSupport if the folder structure should be acquired from model.ArchiveMeta.GetTree\n\tArchiveGet(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (model.Obj, error)\n}\n\ntype ArchiveDecompress interface {\n\tArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error\n}\n\ntype ArchiveDecompressResult interface {\n\t// ArchiveDecompress decompress an archive\n\t// when args.PutIntoNewDir, the new sub-folder should be named the same to the archive but without the extension\n\t// return each decompressed obj from the root path of the archive when args.PutIntoNewDir is false\n\t// return only the newly created folder when args.PutIntoNewDir is true\n\t// return errs.NotImplement to use internal archive tools to decompress\n\tArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error)\n}\n\ntype WithDetails interface {\n\t// GetDetails get storage details (total space, free space, etc.)\n\tGetDetails(ctx context.Context) (*model.StorageDetails, error)\n}\n\ntype Reference interface {\n\tInitReference(storage Driver) error\n}\n\ntype LinkCacheModeResolver interface {\n\t// ResolveLinkCacheMode returns the LinkCacheMode for the given path.\n\tResolveLinkCacheMode(path string) LinkCacheMode\n}\n\ntype DirectUploader interface {\n\t// GetDirectUploadTools returns available frontend-direct upload tools\n\tGetDirectUploadTools() []string\n\t// GetDirectUploadInfo returns the information needed for direct upload from client to storage\n\t// actualPath is the path relative to the storage root (after removing mount path prefix)\n\t// return errs.NotImplement if the driver does not support the given direct upload tool\n\tGetDirectUploadInfo(ctx context.Context, tool string, dstDir model.Obj, fileName string, fileSize int64) (any, error)\n}\n"
  },
  {
    "path": "internal/driver/item.go",
    "content": "package driver\n\ntype Additional interface{}\n\ntype Select string\n\ntype Item struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tDefault  string `json:\"default\"`\n\tOptions  string `json:\"options\"`\n\tRequired bool   `json:\"required\"`\n\tHelp     string `json:\"help\"`\n}\n\ntype Info struct {\n\tCommon     []Item `json:\"common\"`\n\tAdditional []Item `json:\"additional\"`\n\tConfig     Config `json:\"config\"`\n}\n\ntype IRootPath interface {\n\tGetRootPath() string\n}\n\ntype IRootId interface {\n\tGetRootId() string\n}\n\ntype RootPath struct {\n\tRootFolderPath string `json:\"root_folder_path\"`\n}\n\ntype RootID struct {\n\tRootFolderID string `json:\"root_folder_id\"`\n}\n\nfunc (r RootPath) GetRootPath() string {\n\treturn r.RootFolderPath\n}\n\nfunc (r *RootPath) SetRootPath(path string) {\n\tr.RootFolderPath = path\n}\n\nfunc (r RootID) GetRootId() string {\n\treturn r.RootFolderID\n}\n"
  },
  {
    "path": "internal/driver/utils.go",
    "content": "package driver\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n)\n\ntype UpdateProgress = model.UpdateProgress\n\ntype Progress struct {\n\tTotal int64\n\tDone  int64\n\tup    UpdateProgress\n}\n\nfunc (p *Progress) Write(b []byte) (n int, err error) {\n\tn = len(b)\n\tp.Done += int64(n)\n\tp.up(float64(p.Done) / float64(p.Total) * 100)\n\treturn n, err\n}\n\nfunc NewProgress(total int64, up UpdateProgress) *Progress {\n\treturn &Progress{\n\t\tTotal: total,\n\t\tup:    up,\n\t}\n}\n\ntype RateLimitReader = stream.RateLimitReader\n\ntype RateLimitWriter = stream.RateLimitWriter\n\ntype RateLimitFile = stream.RateLimitFile\n\nfunc NewLimitedUploadStream(ctx context.Context, r io.Reader) *RateLimitReader {\n\treturn &RateLimitReader{\n\t\tReader:  r,\n\t\tLimiter: stream.ServerUploadLimit,\n\t\tCtx:     ctx,\n\t}\n}\n\nfunc NewLimitedUploadFile(ctx context.Context, f model.File) *RateLimitFile {\n\treturn &RateLimitFile{\n\t\tFile:    f,\n\t\tLimiter: stream.ServerUploadLimit,\n\t\tCtx:     ctx,\n\t}\n}\n\nfunc ServerUploadLimitWaitN(ctx context.Context, n int) error {\n\treturn stream.ServerUploadLimit.WaitN(ctx, n)\n}\n\ntype ReaderWithCtx = stream.ReaderWithCtx\n\ntype ReaderUpdatingProgress = stream.ReaderUpdatingProgress\n\ntype SimpleReaderWithSize = stream.SimpleReaderWithSize\n"
  },
  {
    "path": "internal/errs/driver.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tEmptyToken = errors.New(\"empty token\")\n)\n"
  },
  {
    "path": "internal/errs/errors.go",
    "content": "package errs\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tpkgerr \"github.com/pkg/errors\"\n)\n\nvar (\n\tNotImplement = errors.New(\"not implement\")\n\tNotSupport   = errors.New(\"not support\")\n\tRelativePath = errors.New(\"using relative path is not allowed\")\n\n\tUploadNotSupported = errors.New(\"upload not supported\")\n\tMetaNotFound       = errors.New(\"meta not found\")\n\tStorageNotFound    = errors.New(\"storage not found\")\n\tStorageNotInit     = errors.New(\"storage not init\")\n\tStreamIncomplete   = errors.New(\"upload/download stream incomplete, possible network issue\")\n\tStreamPeekFail     = errors.New(\"StreamPeekFail\")\n\n\tUnknownArchiveFormat      = errors.New(\"unknown archive format\")\n\tWrongArchivePassword      = errors.New(\"wrong archive password\")\n\tDriverExtractNotSupported = errors.New(\"driver extraction not supported\")\n\n\tWrongShareCode  = errors.New(\"wrong share code\")\n\tInvalidSharing  = errors.New(\"invalid sharing\")\n\tSharingNotFound = errors.New(\"sharing not found\")\n)\n\n// NewErr wrap constant error with an extra message\n// use errors.Is(err1, StorageNotFound) to check if err belongs to any internal error\nfunc NewErr(err error, format string, a ...any) error {\n\treturn fmt.Errorf(\"%w; %s\", err, fmt.Sprintf(format, a...))\n}\n\nfunc IsNotFoundError(err error) bool {\n\treturn errors.Is(pkgerr.Cause(err), ObjectNotFound) || errors.Is(pkgerr.Cause(err), StorageNotFound)\n}\n\nfunc IsNotSupportError(err error) bool {\n\treturn errors.Is(pkgerr.Cause(err), NotSupport)\n}\nfunc IsNotImplementError(err error) bool {\n\treturn errors.Is(pkgerr.Cause(err), NotImplement)\n}\n"
  },
  {
    "path": "internal/errs/errors_test.go",
    "content": "package errs\n\nimport (\n\t\"errors\"\n\tpkgerr \"github.com/pkg/errors\"\n\t\"testing\"\n)\n\nfunc TestErrs(t *testing.T) {\n\n\terr1 := NewErr(StorageNotFound, \"please add a storage first\")\n\tt.Logf(\"err1: %s\", err1)\n\tif !errors.Is(err1, StorageNotFound) {\n\t\tt.Errorf(\"failed, expect %s is %s\", err1, StorageNotFound)\n\t}\n\tif !errors.Is(pkgerr.Cause(err1), StorageNotFound) {\n\t\tt.Errorf(\"failed, expect %s is %s\", err1, StorageNotFound)\n\t}\n\terr2 := pkgerr.WithMessage(err1, \"failed get storage\")\n\tt.Logf(\"err2: %s\", err2)\n\tif !errors.Is(err2, StorageNotFound) {\n\t\tt.Errorf(\"failed, expect %s is %s\", err2, StorageNotFound)\n\t}\n\tif !errors.Is(pkgerr.Cause(err2), StorageNotFound) {\n\t\tt.Errorf(\"failed, expect %s is %s\", err2, StorageNotFound)\n\t}\n}\n"
  },
  {
    "path": "internal/errs/object.go",
    "content": "package errs\n\nimport (\n\t\"errors\"\n\n\tpkgerr \"github.com/pkg/errors\"\n)\n\nvar (\n\tObjectNotFound      = errors.New(\"object not found\")\n\tObjectAlreadyExists = errors.New(\"object already exists\")\n\tNotFolder           = errors.New(\"not a folder\")\n\tNotFile             = errors.New(\"not a file\")\n\tIgnoredSystemFile   = errors.New(\"system file upload ignored\")\n)\n\nfunc IsObjectNotFound(err error) bool {\n\treturn errors.Is(pkgerr.Cause(err), ObjectNotFound)\n}\n"
  },
  {
    "path": "internal/errs/operate.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tPermissionDenied = errors.New(\"permission denied\")\n)\n"
  },
  {
    "path": "internal/errs/search.go",
    "content": "package errs\n\nimport \"fmt\"\n\nvar (\n\tSearchNotAvailable  = fmt.Errorf(\"search not available\")\n\tBuildIndexIsRunning = fmt.Errorf(\"build index is running, please try later\")\n)\n"
  },
  {
    "path": "internal/errs/unwrap.go",
    "content": "package errs\n\nfunc UnwrapOrSelf(err error) error {\n\tu, ok := err.(interface {\n\t\tUnwrap() error\n\t})\n\tif !ok {\n\t\treturn err\n\t}\n\treturn u.Unwrap()\n}\n"
  },
  {
    "path": "internal/errs/user.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tEmptyUsername      = errors.New(\"username is empty\")\n\tEmptyPassword      = errors.New(\"password is empty\")\n\tWrongPassword      = errors.New(\"password is incorrect\")\n\tDeleteAdminOrGuest = errors.New(\"cannot delete admin or guest\")\n)\n"
  },
  {
    "path": "internal/fs/archive.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task_group\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype ArchiveDownloadTask struct {\n\tTaskData\n\tmodel.ArchiveDecompressArgs\n}\n\nfunc (t *ArchiveDownloadTask) GetName() string {\n\treturn fmt.Sprintf(\"decompress [%s](%s)[%s] to [%s](%s) with password <%s>\", t.SrcStorageMp, t.SrcActualPath,\n\t\tt.InnerPath, t.DstStorageMp, t.DstActualPath, t.Password)\n}\n\nfunc (t *ArchiveDownloadTask) Run() error {\n\tif t.SrcStorage == nil {\n\t\tif srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {\n\t\t\tt.SrcStorage = srcStorage\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t\tif dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {\n\t\t\tt.DstStorage = dstStorage\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\tuploadTask, err := t.RunWithoutPushUploadTask()\n\tif err != nil {\n\t\treturn err\n\t}\n\tuploadTask.groupID = stdpath.Join(uploadTask.DstStorageMp, uploadTask.DstActualPath)\n\ttask_group.TransferCoordinator.AddTask(uploadTask.groupID, nil)\n\tArchiveContentUploadTaskManager.Add(uploadTask)\n\treturn nil\n}\n\nfunc (t *ArchiveDownloadTask) RunWithoutPushUploadTask() (*ArchiveContentUploadTask, error) {\n\tsrcObj, tool, ss, err := op.GetArchiveToolAndStream(t.Ctx(), t.SrcStorage, t.SrcActualPath, model.LinkArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tvar e error\n\t\tfor _, s := range ss {\n\t\t\te = stderrors.Join(e, s.Close())\n\t\t}\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", e)\n\t\t}\n\t}()\n\tvar decompressUp model.UpdateProgress\n\tif t.CacheFull {\n\t\ttotal := int64(0)\n\t\tfor _, s := range ss {\n\t\t\ttotal += s.GetSize()\n\t\t}\n\t\tt.SetTotalBytes(total)\n\t\tt.Status = \"getting src object\"\n\t\tpart := 100 / float64(len(ss)+1)\n\t\tfor i, s := range ss {\n\t\t\tif s.GetFile() != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, err = s.CacheFullAndWriter(nil, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\tt.SetProgress(float64(i+1) * part)\n\t\t\t}\n\t\t}\n\t\tdecompressUp = model.UpdateProgressWithRange(t.SetProgress, 100-part, 100)\n\t} else {\n\t\tdecompressUp = t.SetProgress\n\t}\n\tt.Status = \"walking and decompressing\"\n\tdir, err := os.MkdirTemp(conf.Conf.TempDir, \"dir-*\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = tool.Decompress(ss, dir, t.ArchiveInnerArgs, decompressUp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbaseName := strings.TrimSuffix(srcObj.GetName(), stdpath.Ext(srcObj.GetName()))\n\tuploadTask := &ArchiveContentUploadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: t.Creator,\n\t\t\tApiUrl:  t.ApiUrl,\n\t\t},\n\t\tObjName:       baseName,\n\t\tInPlace:       !t.PutIntoNewDir,\n\t\tFilePath:      dir,\n\t\tDstActualPath: t.DstActualPath,\n\t\tdstStorage:    t.DstStorage,\n\t\tDstStorageMp:  t.DstStorageMp,\n\t\toverwrite:     t.Overwrite,\n\t}\n\treturn uploadTask, nil\n}\n\nvar ArchiveDownloadTaskManager *tache.Manager[*ArchiveDownloadTask]\n\ntype ArchiveContentUploadTask struct {\n\ttask.TaskExtension\n\tstatus        string\n\tObjName       string\n\tInPlace       bool\n\tFilePath      string\n\tDstActualPath string\n\tdstStorage    driver.Driver\n\tDstStorageMp  string\n\tfinalized     bool\n\tgroupID       string\n\toverwrite     bool\n}\n\nfunc (t *ArchiveContentUploadTask) GetName() string {\n\treturn fmt.Sprintf(\"upload %s to [%s](%s)\", t.ObjName, t.DstStorageMp, t.DstActualPath)\n}\n\nfunc (t *ArchiveContentUploadTask) GetStatus() string {\n\treturn t.status\n}\n\nfunc (t *ArchiveContentUploadTask) Run() error {\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\treturn t.RunWithNextTaskCallback(func(nextTsk *ArchiveContentUploadTask) error {\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\tArchiveContentUploadTaskManager.Add(nextTsk)\n\t\treturn nil\n\t})\n}\n\nfunc (t *ArchiveContentUploadTask) OnSucceeded() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, true)\n}\n\nfunc (t *ArchiveContentUploadTask) OnFailed() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, false)\n}\n\nfunc (t *ArchiveContentUploadTask) SetRetry(retry int, maxRetry int) {\n\tt.TaskExtension.SetRetry(retry, maxRetry)\n\tif retry == 0 &&\n\t\t(len(t.groupID) == 0 || // 重启恢复\n\t\t\t(t.GetErr() == nil && t.GetState() != tache.StatePending)) { // 手动重试\n\t\tt.groupID = stdpath.Join(t.DstStorageMp, t.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t}\n}\n\nfunc (t *ArchiveContentUploadTask) RunWithNextTaskCallback(f func(nextTask *ArchiveContentUploadTask) error) error {\n\tinfo, err := os.Stat(t.FilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.IsDir() {\n\t\tt.status = \"src object is dir, listing objs\"\n\t\tnextDstActualPath := t.DstActualPath\n\t\tif !t.InPlace {\n\t\t\tnextDstActualPath = stdpath.Join(nextDstActualPath, t.ObjName)\n\t\t\terr = op.MakeDir(t.Ctx(), t.dstStorage, nextDstActualPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tentries, err := os.ReadDir(t.FilePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !t.InPlace {\n\t\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(nextDstActualPath))\n\t\t}\n\t\tvar es error\n\t\tfor _, entry := range entries {\n\t\t\tvar nextFilePath string\n\t\t\tif entry.IsDir() {\n\t\t\t\tnextFilePath, err = moveToTempPath(stdpath.Join(t.FilePath, entry.Name()), \"dir-\")\n\t\t\t} else {\n\t\t\t\tnextFilePath, err = moveToTempPath(stdpath.Join(t.FilePath, entry.Name()), \"file-\")\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tes = stderrors.Join(es, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = f(&ArchiveContentUploadTask{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t\tApiUrl:  t.ApiUrl,\n\t\t\t\t},\n\t\t\t\tObjName:       entry.Name(),\n\t\t\t\tInPlace:       false,\n\t\t\t\tFilePath:      nextFilePath,\n\t\t\t\tDstActualPath: nextDstActualPath,\n\t\t\t\tdstStorage:    t.dstStorage,\n\t\t\t\tDstStorageMp:  t.DstStorageMp,\n\t\t\t\tgroupID:       t.groupID,\n\t\t\t\toverwrite:     t.overwrite,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tes = stderrors.Join(es, err)\n\t\t\t}\n\t\t}\n\t\tif es != nil {\n\t\t\treturn es\n\t\t}\n\t} else {\n\t\tif !t.overwrite {\n\t\t\tdstPath := stdpath.Join(t.DstActualPath, t.ObjName)\n\t\t\tif res, _ := op.Get(t.Ctx(), t.dstStorage, dstPath); res != nil {\n\t\t\t\treturn errs.ObjectAlreadyExists\n\t\t\t}\n\t\t}\n\t\tfile, err := os.Open(t.FilePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tt.SetTotalBytes(info.Size())\n\t\tfs := &stream.FileStream{\n\t\t\tObj: &model.Object{\n\t\t\t\tName:     t.ObjName,\n\t\t\t\tSize:     info.Size(),\n\t\t\t\tModified: time.Now(),\n\t\t\t},\n\t\t\tMimetype:     utils.GetMimeType(stdpath.Ext(t.ObjName)),\n\t\t\tWebPutAsTask: true,\n\t\t\tReader:       file,\n\t\t}\n\t\tfs.Closers.Add(file)\n\t\tt.status = \"uploading\"\n\t\terr = op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.dstStorage, t.DstActualPath, fs, t.SetProgress)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tt.deleteSrcFile()\n\treturn nil\n}\n\nfunc (t *ArchiveContentUploadTask) Cancel() {\n\tt.TaskExtension.Cancel()\n\tif !conf.Conf.Tasks.AllowRetryCanceled {\n\t\tt.deleteSrcFile()\n\t}\n}\n\nfunc (t *ArchiveContentUploadTask) deleteSrcFile() {\n\tif !t.finalized {\n\t\t_ = os.RemoveAll(t.FilePath)\n\t\tt.finalized = true\n\t}\n}\n\nfunc moveToTempPath(path, prefix string) (string, error) {\n\tnewPath, err := genTempFileName(prefix)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = os.Rename(path, newPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn newPath, nil\n}\n\nfunc genTempFileName(prefix string) (string, error) {\n\tretry := 0\n\tt := time.Now().UnixMilli()\n\tfor retry < 10000 {\n\t\tnewPath := filepath.Join(conf.Conf.TempDir, prefix+fmt.Sprintf(\"%x-%x\", t, rand.Uint32()))\n\t\tif _, err := os.Stat(newPath); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn newPath, nil\n\t\t\t} else {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t\tretry++\n\t}\n\treturn \"\", errors.New(\"failed to generate temp-file name: too many retries\")\n}\n\ntype archiveContentUploadTaskManagerType struct {\n\t*tache.Manager[*ArchiveContentUploadTask]\n}\n\nfunc (m *archiveContentUploadTaskManagerType) Remove(id string) {\n\tif t, ok := m.GetByID(id); ok {\n\t\tt.deleteSrcFile()\n\t\tm.Manager.Remove(id)\n\t}\n}\n\nfunc (m *archiveContentUploadTaskManagerType) RemoveAll() {\n\ttasks := m.GetAll()\n\tfor _, t := range tasks {\n\t\tm.Remove(t.GetID())\n\t}\n}\n\nfunc (m *archiveContentUploadTaskManagerType) RemoveByState(state ...tache.State) {\n\ttasks := m.GetByState(state...)\n\tfor _, t := range tasks {\n\t\tm.Remove(t.GetID())\n\t}\n}\n\nfunc (m *archiveContentUploadTaskManagerType) RemoveByCondition(condition func(task *ArchiveContentUploadTask) bool) {\n\ttasks := m.GetByCondition(condition)\n\tfor _, t := range tasks {\n\t\tm.Remove(t.GetID())\n\t}\n}\n\nvar ArchiveContentUploadTaskManager = &archiveContentUploadTaskManagerType{\n\tManager: nil,\n}\n\nfunc archiveMeta(ctx context.Context, path string, args model.ArchiveMetaArgs) (*model.ArchiveMetaProvider, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.GetArchiveMeta(ctx, storage, actualPath, args)\n}\n\nfunc archiveList(ctx context.Context, path string, args model.ArchiveListArgs) ([]model.Obj, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.ListArchive(ctx, storage, actualPath, args)\n}\n\nfunc archiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) (task.TaskExtensionInfo, error) {\n\tsrcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(srcObjPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get src storage\")\n\t}\n\tdstStorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get dst storage\")\n\t}\n\tif srcStorage.GetStorage() == dstStorage.GetStorage() {\n\t\terr = op.ArchiveDecompress(ctx, srcStorage, srcObjActualPath, dstDirActualPath, args, lazyCache...)\n\t\tif !errors.Is(err, errs.NotImplement) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\ttsk := &ArchiveDownloadTask{\n\t\tTaskData: TaskData{\n\t\t\tSrcStorage:    srcStorage,\n\t\t\tDstStorage:    dstStorage,\n\t\t\tSrcActualPath: srcObjActualPath,\n\t\t\tDstActualPath: dstDirActualPath,\n\t\t\tSrcStorageMp:  srcStorage.GetStorage().MountPath,\n\t\t\tDstStorageMp:  dstStorage.GetStorage().MountPath,\n\t\t},\n\t\tArchiveDecompressArgs: args,\n\t}\n\tif ctx.Value(conf.NoTaskKey) != nil {\n\t\ttsk.Base.SetCtx(ctx)\n\t\tuploadTask, err := tsk.RunWithoutPushUploadTask()\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed download [%s]\", srcObjPath)\n\t\t}\n\t\tdefer uploadTask.deleteSrcFile()\n\t\tvar callback func(t *ArchiveContentUploadTask) error\n\t\tvar hasSuccess bool\n\t\tcallback = func(t *ArchiveContentUploadTask) error {\n\t\t\tt.Base.SetCtx(ctx)\n\t\t\te := t.RunWithNextTaskCallback(callback)\n\t\t\tif e == nil {\n\t\t\t\thasSuccess = true\n\t\t\t}\n\t\t\tt.deleteSrcFile()\n\t\t\treturn e\n\t\t}\n\t\tuploadTask.Base.SetCtx(ctx)\n\t\tuploadTask.groupID = stdpath.Join(uploadTask.DstStorageMp, uploadTask.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(uploadTask.groupID, nil)\n\t\terr = uploadTask.RunWithNextTaskCallback(callback)\n\t\ttask_group.TransferCoordinator.Done(context.WithoutCancel(ctx), uploadTask.groupID, hasSuccess)\n\t\treturn nil, err\n\t} else {\n\t\ttsk.Creator, _ = ctx.Value(conf.UserKey).(*model.User)\n\t\ttsk.ApiUrl = common.GetApiUrl(ctx)\n\t\tArchiveDownloadTaskManager.Add(tsk)\n\t\treturn tsk, nil\n\t}\n}\n\nfunc archiveDriverExtract(ctx context.Context, path string, args model.ArchiveInnerArgs) (*model.Link, model.Obj, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.DriverExtract(ctx, storage, actualPath, args)\n}\n\nfunc archiveInternalExtract(ctx context.Context, path string, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, 0, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.InternalExtract(ctx, storage, actualPath, args)\n}\n"
  },
  {
    "path": "internal/fs/copy_move.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task_group\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/pkg/errors\"\n)\n\ntype taskType uint8\n\nfunc (t taskType) String() string {\n\tswitch t {\n\tcase copy:\n\t\treturn \"copy\"\n\tcase move:\n\t\treturn \"move\"\n\tcase merge:\n\t\treturn \"merge\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nconst (\n\tcopy taskType = iota\n\tmove\n\tmerge\n)\n\ntype FileTransferTask struct {\n\tTaskData\n\tTaskType taskType\n\tgroupID  string\n}\n\nfunc (t *FileTransferTask) GetName() string {\n\treturn fmt.Sprintf(\"%s [%s](%s) to [%s](%s)\", t.TaskType, t.SrcStorageMp, t.SrcActualPath, t.DstStorageMp, t.DstActualPath)\n}\n\nfunc (t *FileTransferTask) Run() error {\n\tif t.SrcStorage == nil {\n\t\tif srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {\n\t\t\tt.SrcStorage = srcStorage\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t\tif dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {\n\t\t\tt.DstStorage = dstStorage\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\treturn t.RunWithNextTaskCallback(func(nextTask *FileTransferTask) error {\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\tif t.TaskType == copy || t.TaskType == merge {\n\t\t\tCopyTaskManager.Add(nextTask)\n\t\t} else {\n\t\t\tMoveTaskManager.Add(nextTask)\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (t *FileTransferTask) OnSucceeded() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, true)\n}\n\nfunc (t *FileTransferTask) OnFailed() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, false)\n}\n\nfunc (t *FileTransferTask) SetRetry(retry int, maxRetry int) {\n\tt.TaskData.SetRetry(retry, maxRetry)\n\tif retry == 0 &&\n\t\t(len(t.groupID) == 0 || // 重启恢复\n\t\t\t(t.GetErr() == nil && t.GetState() != tache.StatePending)) { // 手动重试\n\t\tt.groupID = stdpath.Join(t.DstStorageMp, t.DstActualPath)\n\t\tvar payload any\n\t\tif t.TaskType == move {\n\t\t\tpayload = task_group.SrcPathToRemove(stdpath.Join(t.SrcStorageMp, t.SrcActualPath))\n\t\t}\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, payload)\n\t}\n}\n\nfunc transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {\n\tsrcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(srcObjPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get src storage\")\n\t}\n\tdstStorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get dst storage\")\n\t}\n\n\tif srcStorage.GetStorage() == dstStorage.GetStorage() {\n\t\tif utils.IsBool(skipHook...) {\n\t\t\tctx = context.WithValue(ctx, conf.SkipHookKey, struct{}{})\n\t\t}\n\t\tif taskType == copy || taskType == merge {\n\t\t\terr = op.Copy(ctx, srcStorage, srcObjActualPath, dstDirActualPath)\n\t\t\tif !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\terr = op.Move(ctx, srcStorage, srcObjActualPath, dstDirActualPath)\n\t\t\tif !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// not in the same storage\n\tt := &FileTransferTask{\n\t\tTaskData: TaskData{\n\t\t\tSrcStorage:    srcStorage,\n\t\t\tDstStorage:    dstStorage,\n\t\t\tSrcActualPath: srcObjActualPath,\n\t\t\tDstActualPath: dstDirActualPath,\n\t\t\tSrcStorageMp:  srcStorage.GetStorage().MountPath,\n\t\t\tDstStorageMp:  dstStorage.GetStorage().MountPath,\n\t\t},\n\t\tTaskType: taskType,\n\t}\n\n\tt.groupID = stdpath.Join(t.DstStorageMp, t.DstActualPath)\n\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\tif ctx.Value(conf.NoTaskKey) != nil {\n\t\tvar callback func(nextTask *FileTransferTask) error\n\t\thasSuccess := false\n\t\tcallback = func(nextTask *FileTransferTask) error {\n\t\t\tnextTask.Base.SetCtx(ctx)\n\t\t\terr := nextTask.RunWithNextTaskCallback(callback)\n\t\t\tif err == nil {\n\t\t\t\thasSuccess = true\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tt.Base.SetCtx(ctx)\n\t\terr = t.RunWithNextTaskCallback(callback)\n\t\tif err == nil {\n\t\t\thasSuccess = true\n\t\t}\n\t\tif taskType == move {\n\t\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.SrcPathToRemove(srcObjPath))\n\t\t}\n\t\ttask_group.TransferCoordinator.Done(context.WithoutCancel(ctx), t.groupID, hasSuccess)\n\t\treturn nil, err\n\t}\n\n\tt.Creator, _ = ctx.Value(conf.UserKey).(*model.User)\n\tt.ApiUrl = common.GetApiUrl(ctx)\n\tif taskType == copy || taskType == merge {\n\t\tCopyTaskManager.Add(t)\n\t} else {\n\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.SrcPathToRemove(srcObjPath))\n\t\tMoveTaskManager.Add(t)\n\t}\n\treturn t, nil\n}\n\nfunc (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransferTask) error) error {\n\tt.Status = \"getting src object\"\n\tsrcObj, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcActualPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", t.SrcActualPath)\n\t}\n\n\tif srcObj.IsDir() {\n\t\tt.Status = \"src object is dir, listing objs\"\n\t\tobjs, err := op.List(t.Ctx(), t.SrcStorage, t.SrcActualPath, model.ListArgs{})\n\t\tif err != nil {\n\t\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", t.SrcActualPath)\n\t\t}\n\t\tdstActualPath := stdpath.Join(t.DstActualPath, srcObj.GetName())\n\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(dstActualPath))\n\n\t\texistedObjs := make(map[string]bool)\n\t\tif t.TaskType == merge {\n\t\t\tdstObjs, err := op.List(t.Ctx(), t.DstStorage, dstActualPath, model.ListArgs{})\n\t\t\tif err != nil && !errors.Is(err, errs.ObjectNotFound) {\n\t\t\t\t// 目标文件夹不存在的情况不是错误，会在之后新建文件夹\n\t\t\t\t// 这种情况显然不需要统计existedObjs，dstObjs保持为nil，下面这个for将不会执行\n\t\t\t\treturn errors.WithMessagef(err, \"failed list dst [%s] objs\", dstActualPath)\n\t\t\t}\n\t\t\tfor _, obj := range dstObjs {\n\t\t\t\tif err := t.Ctx().Err(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif !obj.IsDir() {\n\t\t\t\t\texistedObjs[obj.GetName()] = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, obj := range objs {\n\t\t\tif err := t.Ctx().Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif t.TaskType == merge && !obj.IsDir() && existedObjs[obj.GetName()] {\n\t\t\t\t// skip existed file\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terr = f(&FileTransferTask{\n\t\t\t\tTaskType: t.TaskType,\n\t\t\t\tTaskData: TaskData{\n\t\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t\t\tApiUrl:  t.ApiUrl,\n\t\t\t\t\t},\n\t\t\t\t\tSrcStorage:    t.SrcStorage,\n\t\t\t\t\tDstStorage:    t.DstStorage,\n\t\t\t\t\tSrcActualPath: stdpath.Join(t.SrcActualPath, obj.GetName()),\n\t\t\t\t\tDstActualPath: dstActualPath,\n\t\t\t\t\tSrcStorageMp:  t.SrcStorageMp,\n\t\t\t\t\tDstStorageMp:  t.DstStorageMp,\n\t\t\t\t},\n\t\t\t\tgroupID: t.groupID,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tt.Status = fmt.Sprintf(\"src object is dir, added all %s tasks of objs\", t.TaskType)\n\t\treturn nil\n\t}\n\n\tt.Status = \"getting src object link\"\n\tlink, srcObj, err := op.Link(t.Ctx(), t.SrcStorage, t.SrcActualPath, model.LinkArgs{})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] link\", t.SrcActualPath)\n\t}\n\t// any link provided is seekable\n\tss, err := stream.NewSeekableStream(&stream.FileStream{\n\t\tObj: srcObj,\n\t\tCtx: t.Ctx(),\n\t}, link)\n\tif err != nil {\n\t\t_ = link.Close()\n\t\treturn errors.WithMessagef(err, \"failed get [%s] stream\", t.SrcActualPath)\n\t}\n\tt.SetTotalBytes(ss.GetSize())\n\tt.Status = \"uploading\"\n\treturn op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.DstStorage, t.DstActualPath, ss, t.SetProgress)\n}\n\nvar (\n\tCopyTaskManager *tache.Manager[*FileTransferTask]\n\tMoveTaskManager *tache.Manager[*FileTransferTask]\n)\n"
  },
  {
    "path": "internal/fs/fs.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/pkg/errors\"\n)\n\n// the param named path of functions in this package is a mount path\n// So, the purpose of this package is to convert mount path to actual path\n// then pass the actual path to the op package\n\ntype ListArgs struct {\n\tRefresh            bool\n\tNoLog              bool\n\tWithStorageDetails bool\n}\n\nfunc List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {\n\tres, err := list(ctx, path, args)\n\tif err != nil {\n\t\tif !args.NoLog {\n\t\t\tlog.Errorf(\"failed list %s: %+v\", path, err)\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn res, nil\n}\n\ntype GetArgs struct {\n\tNoLog              bool\n\tWithStorageDetails bool\n}\n\nfunc Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {\n\tres, err := get(ctx, path, args)\n\tif err != nil {\n\t\tif !args.NoLog {\n\t\t\tlog.Warnf(\"failed get %s: %s\", path, err)\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn res, nil\n}\n\nfunc Link(ctx context.Context, path string, args model.LinkArgs) (*model.Link, model.Obj, error) {\n\tres, file, err := link(ctx, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed link %s: %+v\", path, err)\n\t\treturn nil, nil, err\n\t}\n\treturn res, file, nil\n}\n\nfunc MakeDir(ctx context.Context, path string) error {\n\terr := makeDir(ctx, path)\n\tif err != nil {\n\t\tlog.Errorf(\"failed make dir %s: %+v\", path, err)\n\t}\n\treturn err\n}\n\nfunc Move(ctx context.Context, srcPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {\n\treq, err := transfer(ctx, move, srcPath, dstDirPath, skipHook...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed move %s to %s: %+v\", srcPath, dstDirPath, err)\n\t}\n\treturn req, err\n}\n\nfunc Copy(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {\n\tres, err := transfer(ctx, copy, srcObjPath, dstDirPath, skipHook...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed copy %s to %s: %+v\", srcObjPath, dstDirPath, err)\n\t}\n\treturn res, err\n}\n\nfunc Merge(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {\n\tres, err := transfer(ctx, merge, srcObjPath, dstDirPath, skipHook...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed merge %s to %s: %+v\", srcObjPath, dstDirPath, err)\n\t}\n\treturn res, err\n}\n\nfunc Rename(ctx context.Context, srcPath, dstName string, skipHook ...bool) error {\n\terr := rename(ctx, srcPath, dstName, skipHook...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed rename %s to %s: %+v\", srcPath, dstName, err)\n\t}\n\treturn err\n}\n\nfunc Remove(ctx context.Context, path string) error {\n\terr := remove(ctx, path)\n\tif err != nil {\n\t\tlog.Errorf(\"failed remove %s: %+v\", path, err)\n\t}\n\treturn err\n}\n\nfunc PutDirectly(ctx context.Context, dstDirPath string, file model.FileStreamer, skipHook ...bool) error {\n\terr := putDirectly(ctx, dstDirPath, file, skipHook...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed put %s: %+v\", dstDirPath, err)\n\t}\n\treturn err\n}\n\nfunc PutAsTask(ctx context.Context, dstDirPath string, file model.FileStreamer) (task.TaskExtensionInfo, error) {\n\tt, err := putAsTask(ctx, dstDirPath, file)\n\tif err != nil {\n\t\tlog.Errorf(\"failed put %s: %+v\", dstDirPath, err)\n\t}\n\treturn t, err\n}\n\nfunc ArchiveMeta(ctx context.Context, path string, args model.ArchiveMetaArgs) (*model.ArchiveMetaProvider, error) {\n\tmeta, err := archiveMeta(ctx, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed get archive meta %s: %+v\", path, err)\n\t}\n\treturn meta, err\n}\n\nfunc ArchiveList(ctx context.Context, path string, args model.ArchiveListArgs) ([]model.Obj, error) {\n\tobjs, err := archiveList(ctx, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed list archive [%s]%s: %+v\", path, args.InnerPath, err)\n\t}\n\treturn objs, err\n}\n\nfunc ArchiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) (task.TaskExtensionInfo, error) {\n\tt, err := archiveDecompress(ctx, srcObjPath, dstDirPath, args, lazyCache...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed decompress [%s]%s: %+v\", srcObjPath, args.InnerPath, err)\n\t}\n\treturn t, err\n}\n\nfunc ArchiveDriverExtract(ctx context.Context, path string, args model.ArchiveInnerArgs) (*model.Link, model.Obj, error) {\n\tl, obj, err := archiveDriverExtract(ctx, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed extract [%s]%s: %+v\", path, args.InnerPath, err)\n\t}\n\treturn l, obj, err\n}\n\nfunc ArchiveInternalExtract(ctx context.Context, path string, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\tl, obj, err := archiveInternalExtract(ctx, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed extract [%s]%s: %+v\", path, args.InnerPath, err)\n\t}\n\treturn l, obj, err\n}\n\ntype GetStoragesArgs struct {\n}\n\nfunc GetStorage(path string, args *GetStoragesArgs) (driver.Driver, error) {\n\tstorageDriver, _, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn storageDriver, nil\n}\n\nfunc Other(ctx context.Context, args model.FsOtherArgs) (interface{}, error) {\n\tres, err := other(ctx, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed get other %s: %+v\", args.Path, err)\n\t}\n\treturn res, err\n}\n\nfunc PutURL(ctx context.Context, path, dstName, urlStr string) error {\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif storage.Config().NoUpload {\n\t\treturn errors.WithStack(errs.UploadNotSupported)\n\t}\n\t_, ok := storage.(driver.PutURL)\n\t_, okResult := storage.(driver.PutURLResult)\n\tif !ok && !okResult {\n\t\treturn errs.NotImplement\n\t}\n\treturn op.PutURL(ctx, storage, dstDirActualPath, dstName, urlStr)\n}\n\nfunc GetDirectUploadInfo(ctx context.Context, tool, path, dstName string, fileSize int64) (any, error) {\n\tinfo, err := getDirectUploadInfo(ctx, tool, path, dstName, fileSize)\n\tif err != nil {\n\t\tlog.Errorf(\"failed get %s direct upload info for %s(%d bytes): %+v\", path, dstName, fileSize, err)\n\t}\n\treturn info, err\n}\n"
  },
  {
    "path": "internal/fs/get.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {\n\tpath = utils.FixAndCleanPath(path)\n\t// maybe a virtual file\n\tif path != \"/\" {\n\t\tdir, name := stdpath.Split(path)\n\t\tvirtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, dir, !args.WithStorageDetails, false, name)\n\t\tfor _, f := range virtualFiles {\n\t\t\tif f.GetName() == name {\n\t\t\t\treturn f, nil\n\t\t\t}\n\t\t}\n\t}\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\t// if there are no storage prefix with path, maybe root folder\n\t\tif path == \"/\" {\n\t\t\treturn &model.Object{\n\t\t\t\tName:     \"root\",\n\t\t\t\tIsFolder: true,\n\t\t\t\tMask:     model.ReadOnly | model.Virtual,\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.Get(ctx, storage, actualPath)\n}\n"
  },
  {
    "path": "internal/fs/link.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc link(ctx context.Context, path string, args model.LinkArgs) (*model.Link, model.Obj, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\tl, obj, err := op.Link(ctx, storage, actualPath, args)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithMessage(err, \"failed link\")\n\t}\n\tif l.URL != \"\" && !strings.HasPrefix(l.URL, \"http://\") && !strings.HasPrefix(l.URL, \"https://\") {\n\t\tl.URL = common.GetApiUrl(ctx) + l.URL\n\t}\n\treturn l, obj, nil\n}\n"
  },
  {
    "path": "internal/fs/list.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// List files\nfunc list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {\n\tmeta, _ := ctx.Value(conf.MetaKey).(*model.Meta)\n\tuser, _ := ctx.Value(conf.UserKey).(*model.User)\n\tvirtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, path, !args.WithStorageDetails, args.Refresh, \"\")\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil && len(virtualFiles) == 0 {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\n\tvar _objs []model.Obj\n\tif storage != nil {\n\t\t_objs, err = op.List(ctx, storage, actualPath, model.ListArgs{\n\t\t\tReqPath:            path,\n\t\t\tRefresh:            args.Refresh,\n\t\t\tWithStorageDetails: args.WithStorageDetails,\n\t\t})\n\t\tif err != nil {\n\t\t\tif !args.NoLog {\n\t\t\t\tlog.Errorf(\"fs/list: %+v\", err)\n\t\t\t}\n\t\t\tif len(virtualFiles) == 0 {\n\t\t\t\treturn nil, errors.WithMessage(err, \"failed get objs\")\n\t\t\t}\n\t\t}\n\t}\n\n\tom := model.NewObjMerge()\n\tif whetherHide(user, meta, path) {\n\t\tom.InitHideReg(meta.Hide)\n\t}\n\tobjs := om.Merge(_objs, virtualFiles...)\n\treturn objs, nil\n}\n\nfunc whetherHide(user *model.User, meta *model.Meta, path string) bool {\n\t// if is admin, don't hide\n\tif user == nil || user.CanSeeHides() {\n\t\treturn false\n\t}\n\t// if meta is nil, don't hide\n\tif meta == nil {\n\t\treturn false\n\t}\n\t// if meta.Hide is empty, don't hide\n\tif meta.Hide == \"\" {\n\t\treturn false\n\t}\n\t// if meta doesn't apply to sub_folder, don't hide\n\tif !utils.PathEqual(meta.Path, path) && !meta.HSub {\n\t\treturn false\n\t}\n\t// if is guest, hide\n\treturn true\n}\n"
  },
  {
    "path": "internal/fs/other.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc makeDir(ctx context.Context, path string) error {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.MakeDir(ctx, storage, actualPath)\n}\n\nfunc rename(ctx context.Context, srcPath, dstName string, skipHook ...bool) error {\n\tstorage, srcActualPath, err := op.GetStorageAndActualPath(srcPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif utils.IsBool(skipHook...) {\n\t\tctx = context.WithValue(ctx, conf.SkipHookKey, struct{}{})\n\t}\n\treturn op.Rename(ctx, storage, srcActualPath, dstName)\n}\n\nfunc remove(ctx context.Context, path string) error {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(path)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.Remove(ctx, storage, actualPath)\n}\n\nfunc other(ctx context.Context, args model.FsOtherArgs) (interface{}, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.Path)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\targs.Path = actualPath\n\treturn op.Other(ctx, storage, args)\n}\n\ntype TaskData struct {\n\ttask.TaskExtension\n\tStatus        string        `json:\"-\"` //don't save status to save space\n\tSrcActualPath string        `json:\"src_path\"`\n\tDstActualPath string        `json:\"dst_path\"`\n\tSrcStorage    driver.Driver `json:\"-\"`\n\tDstStorage    driver.Driver `json:\"-\"`\n\tSrcStorageMp  string        `json:\"src_storage_mp\"`\n\tDstStorageMp  string        `json:\"dst_storage_mp\"`\n}\n\nfunc (t *TaskData) GetStatus() string {\n\treturn t.Status\n}\n"
  },
  {
    "path": "internal/fs/put.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task_group\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/pkg/errors\"\n)\n\ntype UploadTask struct {\n\ttask.TaskExtension\n\tstorage          driver.Driver\n\tdstDirActualPath string\n\tfile             model.FileStreamer\n}\n\nfunc (t *UploadTask) GetName() string {\n\treturn fmt.Sprintf(\"upload %s to [%s](%s)\", t.file.GetName(), t.storage.GetStorage().MountPath, t.dstDirActualPath)\n}\n\nfunc (t *UploadTask) GetStatus() string {\n\treturn \"uploading\"\n}\n\nfunc (t *UploadTask) Run() error {\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\treturn op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.storage, t.dstDirActualPath, t.file, t.SetProgress)\n}\n\nfunc (t *UploadTask) OnSucceeded() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), stdpath.Join(t.storage.GetStorage().MountPath, t.dstDirActualPath), true)\n}\n\nfunc (t *UploadTask) OnFailed() {\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), stdpath.Join(t.storage.GetStorage().MountPath, t.dstDirActualPath), false)\n}\n\nfunc (t *UploadTask) SetRetry(retry int, maxRetry int) {\n\tt.TaskExtension.SetRetry(retry, maxRetry)\n\tif retry == 0 &&\n\t\t(t.GetErr() == nil && t.GetState() != tache.StatePending) { // 手动重试\n\t\ttask_group.TransferCoordinator.AddTask(stdpath.Join(t.storage.GetStorage().MountPath, t.dstDirActualPath), nil)\n\t}\n}\n\nvar UploadTaskManager *tache.Manager[*UploadTask]\n\n// putAsTask add as a put task and return immediately\nfunc putAsTask(ctx context.Context, dstDirPath string, file model.FileStreamer) (task.TaskExtensionInfo, error) {\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif storage.Config().NoUpload {\n\t\treturn nil, errors.WithStack(errs.UploadNotSupported)\n\t}\n\tif file.NeedStore() {\n\t\t_, err := file.CacheFullAndWriter(nil, nil)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed to create temp file\")\n\t\t}\n\t\t//file.SetReader(tempFile)\n\t\t//file.SetTmpFile(tempFile)\n\t}\n\ttaskCreator, _ := ctx.Value(conf.UserKey).(*model.User) // taskCreator is nil when convert failed\n\tt := &UploadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\n\t\t\tApiUrl:  common.GetApiUrl(ctx),\n\t\t},\n\t\tstorage:          storage,\n\t\tdstDirActualPath: dstDirActualPath,\n\t\tfile:             file,\n\t}\n\tt.SetTotalBytes(file.GetSize())\n\ttask_group.TransferCoordinator.AddTask(stdpath.Join(storage.GetStorage().MountPath, dstDirActualPath), nil)\n\tUploadTaskManager.Add(t)\n\treturn t, nil\n}\n\n// putDirect put the file and return after finish\nfunc putDirectly(ctx context.Context, dstDirPath string, file model.FileStreamer, skipHook ...bool) error {\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\t_ = file.Close()\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif storage.Config().NoUpload {\n\t\t_ = file.Close()\n\t\treturn errors.WithStack(errs.UploadNotSupported)\n\t}\n\tif utils.IsBool(skipHook...) {\n\t\tctx = context.WithValue(ctx, conf.SkipHookKey, struct{}{})\n\t}\n\treturn op.Put(ctx, storage, dstDirActualPath, file, nil)\n}\n\nfunc getDirectUploadInfo(ctx context.Context, tool, dstDirPath, dstName string, fileSize int64) (any, error) {\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn op.GetDirectUploadInfo(ctx, tool, storage, dstDirActualPath, dstName, fileSize)\n}\n"
  },
  {
    "path": "internal/fs/walk.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\n// WalkFS traverses filesystem fs starting at name up to depth levels.\n//\n// WalkFS will stop when current depth > `depth`. For each visited node,\n// WalkFS calls walkFn. If a visited file system node is a directory and\n// walkFn returns path.SkipDir, walkFS will skip traversal of this node.\nfunc WalkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn func(reqPath string, info model.Obj) error) error {\n\t// This implementation is based on Walk's code in the standard path/path package.\n\twalkFnErr := walkFn(name, info)\n\tif walkFnErr != nil {\n\t\tif info.IsDir() && walkFnErr == filepath.SkipDir {\n\t\t\treturn nil\n\t\t}\n\t\treturn walkFnErr\n\t}\n\tif !info.IsDir() || depth == 0 {\n\t\treturn nil\n\t}\n\tmeta, _ := op.GetNearestMeta(name)\n\t// Read directory names.\n\tobjs, err := List(context.WithValue(ctx, conf.MetaKey, meta), name, &ListArgs{})\n\tif err != nil {\n\t\treturn walkFnErr\n\t}\n\tfor _, fileInfo := range objs {\n\t\tfilename := path.Join(name, fileInfo.GetName())\n\t\tif err := WalkFS(ctx, depth-1, filename, fileInfo, walkFn); err != nil {\n\t\t\tif err == filepath.SkipDir {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/fuse/fs.go",
    "content": "package fuse\n\nimport \"github.com/winfsp/cgofuse/fuse\"\n\ntype Fs struct {\n\tRootFolder string\n\tfuse.FileSystemBase\n}\n\nfunc (fs *Fs) Init() {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Destroy() {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Statfs(path string, stat *fuse.Statfs_t) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Mknod(path string, mode uint32, dev uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Mkdir(path string, mode uint32) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Unlink(path string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Rmdir(path string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Link(oldpath string, newpath string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Symlink(target string, newpath string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Readlink(path string) (int, string) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Rename(oldpath string, newpath string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Chmod(path string, mode uint32) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Chown(path string, uid uint32, gid uint32) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Utimens(path string, tmsp []fuse.Timespec) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Access(path string, mask uint32) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Create(path string, flags int, mode uint32) (int, uint64) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Open(path string, flags int) (int, uint64) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Truncate(path string, size int64, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Read(path string, buff []byte, ofst int64, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Write(path string, buff []byte, ofst int64, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Flush(path string, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Release(path string, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Fsync(path string, datasync bool, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Opendir(path string) (int, uint64) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Readdir(path string, fill func(name string, stat *fuse.Stat_t, ofst int64) bool, ofst int64, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Releasedir(path string, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Fsyncdir(path string, datasync bool, fh uint64) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Setxattr(path string, name string, value []byte, flags int) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Getxattr(path string, name string) (int, []byte) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Removexattr(path string, name string) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (fs *Fs) Listxattr(path string, fill func(name string) bool) int {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nvar _ fuse.FileSystemInterface = (*Fs)(nil)\n"
  },
  {
    "path": "internal/fuse/mount.go",
    "content": "package fuse\n\nimport \"github.com/winfsp/cgofuse/fuse\"\n\nfunc Mount(mountSrc, mountDst string, opts []string) {\n\tfs := &Fs{RootFolder: mountSrc}\n\thost := fuse.NewFileSystemHost(fs)\n\tgo host.Mount(mountDst, opts)\n}\n"
  },
  {
    "path": "internal/message/http.go",
    "content": "package message\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\ntype Http struct {\n\tReceived chan string  // received messages from web\n\tToSend   chan Message // messages to send to web\n}\n\ntype Req struct {\n\tMessage string `json:\"message\" form:\"message\"`\n}\n\nfunc (p *Http) GetHandle(c *gin.Context) {\n\tselect {\n\tcase message := <-p.ToSend:\n\t\tcommon.SuccessResp(c, message)\n\tdefault:\n\t\tcommon.ErrorStrResp(c, \"no message\", 404)\n\t}\n}\n\nfunc (p *Http) SendHandle(c *gin.Context) {\n\tvar req Req\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tselect {\n\tcase p.Received <- req.Message:\n\t\tcommon.SuccessResp(c)\n\tdefault:\n\t\tcommon.ErrorStrResp(c, \"nowhere needed\", 500)\n\t}\n}\n\nfunc (p *Http) Send(message Message) error {\n\tselect {\n\tcase p.ToSend <- message:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"send failed\")\n\t}\n}\n\nfunc (p *Http) Receive() (string, error) {\n\tselect {\n\tcase message := <-p.Received:\n\t\treturn message, nil\n\tdefault:\n\t\treturn \"\", errors.New(\"receive failed\")\n\t}\n}\n\nfunc (p *Http) WaitSend(message Message, d int) error {\n\tselect {\n\tcase p.ToSend <- message:\n\t\treturn nil\n\tcase <-time.After(time.Duration(d) * time.Second):\n\t\treturn errors.New(\"send timeout\")\n\t}\n}\n\nfunc (p *Http) WaitReceive(d int) (string, error) {\n\tselect {\n\tcase message := <-p.Received:\n\t\treturn message, nil\n\tcase <-time.After(time.Duration(d) * time.Second):\n\t\treturn \"\", errors.New(\"receive timeout\")\n\t}\n}\n\nvar HttpInstance = &Http{\n\tReceived: make(chan string),\n\tToSend:   make(chan Message),\n}\n"
  },
  {
    "path": "internal/message/message.go",
    "content": "package message\n\ntype Message struct {\n\tType    string      `json:\"type\"`\n\tContent interface{} `json:\"content\"`\n}\n\ntype Messenger interface {\n\tSend(Message) error\n\tReceive() (string, error)\n\tWaitSend(Message, int) error\n\tWaitReceive(int) (string, error)\n}\n\nfunc GetMessenger() Messenger {\n\treturn HttpInstance\n}\n"
  },
  {
    "path": "internal/message/ws.go",
    "content": "package message\n\n// TODO websocket implementation\n"
  },
  {
    "path": "internal/model/archive.go",
    "content": "package model\n\nimport \"time\"\n\ntype ObjTree interface {\n\tObj\n\tGetChildren() []ObjTree\n}\n\ntype ObjectTree struct {\n\tObject\n\tChildren []ObjTree\n}\n\nfunc (t *ObjectTree) GetChildren() []ObjTree {\n\treturn t.Children\n}\n\ntype ArchiveMeta interface {\n\tGetComment() string\n\t// IsEncrypted means if the content of the archive requires a password to access\n\t// GetArchiveMeta should return errs.WrongArchivePassword if the meta-info is also encrypted,\n\t// and the provided password is empty.\n\tIsEncrypted() bool\n\t// GetTree directly returns the full folder structure\n\t// returns nil if the folder structure should be acquired by calling driver.ArchiveReader.ListArchive\n\tGetTree() []ObjTree\n}\n\ntype ArchiveMetaInfo struct {\n\tComment   string\n\tEncrypted bool\n\tTree      []ObjTree\n}\n\nfunc (m *ArchiveMetaInfo) GetComment() string {\n\treturn m.Comment\n}\n\nfunc (m *ArchiveMetaInfo) IsEncrypted() bool {\n\treturn m.Encrypted\n}\n\nfunc (m *ArchiveMetaInfo) GetTree() []ObjTree {\n\treturn m.Tree\n}\n\ntype ArchiveMetaProvider struct {\n\tArchiveMeta\n\t*Sort\n\tDriverProviding bool\n\tExpiration      *time.Duration\n}\n"
  },
  {
    "path": "internal/model/args.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ListArgs struct {\n\tReqPath            string\n\tS3ShowPlaceholder  bool\n\tRefresh            bool\n\tWithStorageDetails bool\n\tSkipHook           bool\n}\n\ntype LinkArgs struct {\n\tIP       string\n\tHeader   http.Header\n\tType     string\n\tRedirect bool\n}\n\ntype Link struct {\n\tURL         string        `json:\"url\"`    // most common way\n\tHeader      http.Header   `json:\"header\"` // needed header (for url)\n\tRangeReader RangeReaderIF `json:\"-\"`      // recommended way if can't use URL\n\n\tExpiration *time.Duration // local cache expire Duration\n\n\t//for accelerating request, use multi-thread downloading\n\tConcurrency   int   `json:\"concurrency\"`\n\tPartSize      int   `json:\"part_size\"`\n\tContentLength int64 `json:\"content_length\"` // 转码视频、缩略图\n\n\tutils.SyncClosers `json:\"-\"`\n\t// 如果SyncClosers中的资源被关闭后Link将不可用，则此值应为 true\n\tRequireReference bool `json:\"-\"`\n}\n\ntype OtherArgs struct {\n\tObj    Obj\n\tMethod string\n\tData   interface{}\n}\n\ntype FsOtherArgs struct {\n\tPath   string      `json:\"path\" form:\"path\"`\n\tMethod string      `json:\"method\" form:\"method\"`\n\tData   interface{} `json:\"data\" form:\"data\"`\n}\n\ntype ArchiveArgs struct {\n\tPassword string\n\tLinkArgs\n}\n\ntype ArchiveInnerArgs struct {\n\tArchiveArgs\n\tInnerPath string\n}\n\ntype ArchiveMetaArgs struct {\n\tArchiveArgs\n\tRefresh bool\n}\n\ntype ArchiveListArgs struct {\n\tArchiveInnerArgs\n\tRefresh bool\n}\n\ntype ArchiveDecompressArgs struct {\n\tArchiveInnerArgs\n\tCacheFull     bool\n\tPutIntoNewDir bool\n\tOverwrite     bool\n}\n\ntype SharingListArgs struct {\n\tRefresh bool\n\tPwd     string\n}\n\ntype SharingArchiveMetaArgs struct {\n\tArchiveMetaArgs\n\tPwd string\n}\n\ntype SharingArchiveListArgs struct {\n\tArchiveListArgs\n\tPwd string\n}\n\ntype SharingLinkArgs struct {\n\tPwd string\n\tLinkArgs\n}\n\ntype RangeReaderIF interface {\n\tRangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error)\n}\n\ntype RangeReadCloserIF interface {\n\tRangeReaderIF\n\tutils.ClosersIF\n}\n\nvar _ RangeReadCloserIF = (*RangeReadCloser)(nil)\n\ntype RangeReadCloser struct {\n\tRangeReader RangeReaderIF\n\tutils.Closers\n}\n\nfunc (r *RangeReadCloser) RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\trc, err := r.RangeReader.RangeRead(ctx, httpRange)\n\tr.Add(rc)\n\treturn rc, err\n}\n"
  },
  {
    "path": "internal/model/direct_upload.go",
    "content": "package model\n\ntype HttpDirectUploadInfo struct {\n\tUploadURL string            `json:\"upload_url\"`        // The URL to upload the file\n\tChunkSize int64             `json:\"chunk_size\"`        // The chunk size for uploading, 0 means no chunking required\n\tHeaders   map[string]string `json:\"headers,omitempty\"` // Optional headers to include in the upload request\n\tMethod    string            `json:\"method,omitempty\"`  // HTTP method, default is PUT\n}\n"
  },
  {
    "path": "internal/model/file.go",
    "content": "package model\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// File is basic file level accessing interface\ntype File interface {\n\tio.Reader\n\tio.ReaderAt\n\tio.Seeker\n}\ntype FileCloser struct {\n\tFile\n\tio.Closer\n}\n\nfunc (f *FileCloser) Close() error {\n\tvar errs []error\n\tif clr, ok := f.File.(io.Closer); ok {\n\t\terrs = append(errs, clr.Close())\n\t}\n\tif f.Closer != nil {\n\t\terrs = append(errs, f.Closer.Close())\n\t}\n\treturn errors.Join(errs...)\n}\n\n// FileRangeReader 是对 RangeReaderIF 的轻量包装，表明由 RangeReaderIF.RangeRead\n// 返回的 io.ReadCloser 同时实现了 model.File（即支持 Read/ReadAt/Seek）。\n// 只有满足这些才需要使用 FileRangeReader，否则直接使用 RangeReaderIF 即可。\ntype FileRangeReader struct {\n\tRangeReaderIF\n}\n"
  },
  {
    "path": "internal/model/meta.go",
    "content": "package model\n\ntype Meta struct {\n\tID        uint   `json:\"id\" gorm:\"primaryKey\"`\n\tPath      string `json:\"path\" gorm:\"unique\" binding:\"required\"`\n\tPassword  string `json:\"password\"`\n\tPSub      bool   `json:\"p_sub\"`\n\tWrite     bool   `json:\"write\"`\n\tWSub      bool   `json:\"w_sub\"`\n\tHide      string `json:\"hide\"`\n\tHSub      bool   `json:\"h_sub\"`\n\tReadme    string `json:\"readme\"`\n\tRSub      bool   `json:\"r_sub\"`\n\tHeader    string `json:\"header\"`\n\tHeaderSub bool   `json:\"header_sub\"`\n}\n"
  },
  {
    "path": "internal/model/obj.go",
    "content": "package model\n\nimport (\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/dlclark/regexp2\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\n\t\"github.com/maruel/natural\"\n)\n\ntype ObjUnwrap interface {\n\tUnwrap() Obj\n}\n\ntype Obj interface {\n\tGetSize() int64\n\tGetName() string\n\tModTime() time.Time\n\tCreateTime() time.Time\n\tIsDir() bool\n\tGetHash() utils.HashInfo\n\n\t// The internal information of the driver.\n\t// If you want to use it, please understand what it means\n\tGetID() string\n\tGetPath() string\n}\n\n// FileStreamer ->check FileStream for more comments\ntype FileStreamer interface {\n\tio.Reader\n\tutils.ClosersIF\n\tObj\n\tGetMimetype() string\n\tNeedStore() bool\n\tIsForceStreamUpload() bool\n\tGetExist() Obj\n\tSetExist(Obj)\n\t// for a non-seekable Stream, RangeRead supports peeking some data, and CacheFullAndWriter still works\n\tRangeRead(http_range.Range) (io.Reader, error)\n\t// for a non-seekable Stream, if Read is called, this function won't work.\n\t// caches the full Stream and writes it to writer (if provided, even if the stream is already cached).\n\tCacheFullAndWriter(up *UpdateProgress, writer io.Writer) (File, error)\n\t// if the Stream is not a File and is not cached, returns nil.\n\tGetFile() File\n}\n\ntype UpdateProgress func(percentage float64)\n\nfunc UpdateProgressWithRange(inner UpdateProgress, start, end float64) UpdateProgress {\n\treturn func(p float64) {\n\t\tif p < 0 {\n\t\t\tp = 0\n\t\t}\n\t\tif p > 100 {\n\t\t\tp = 100\n\t\t}\n\t\tscaled := start + (end-start)*(p/100.0)\n\t\tinner(scaled)\n\t}\n}\n\ntype URL interface {\n\tURL() string\n}\n\ntype Thumb interface {\n\tThumb() string\n}\n\ntype SetPath interface {\n\tSetPath(path string)\n}\n\ntype ObjWithProvider interface {\n\tGetProvider() string\n}\n\nfunc SortFiles(objs []Obj, orderBy, orderDirection string) {\n\tif orderBy == \"\" {\n\t\treturn\n\t}\n\tsort.Slice(objs, func(i, j int) bool {\n\t\tswitch orderBy {\n\t\tcase \"name\":\n\t\t\t{\n\t\t\t\tc := natural.Less(objs[i].GetName(), objs[j].GetName())\n\t\t\t\tif orderDirection == \"desc\" {\n\t\t\t\t\treturn !c\n\t\t\t\t}\n\t\t\t\treturn c\n\t\t\t}\n\t\tcase \"size\":\n\t\t\t{\n\t\t\t\tif orderDirection == \"desc\" {\n\t\t\t\t\treturn objs[i].GetSize() >= objs[j].GetSize()\n\t\t\t\t}\n\t\t\t\treturn objs[i].GetSize() <= objs[j].GetSize()\n\t\t\t}\n\t\tcase \"modified\":\n\t\t\tif orderDirection == \"desc\" {\n\t\t\t\treturn objs[i].ModTime().After(objs[j].ModTime())\n\t\t\t}\n\t\t\treturn objs[i].ModTime().Before(objs[j].ModTime())\n\t\t}\n\t\treturn false\n\t})\n}\n\nfunc ExtractFolder(objs []Obj, extractFolder string) {\n\tif extractFolder == \"\" {\n\t\treturn\n\t}\n\tfront := extractFolder == \"front\"\n\tsort.SliceStable(objs, func(i, j int) bool {\n\t\tif objs[i].IsDir() || objs[j].IsDir() {\n\t\t\tif !objs[i].IsDir() {\n\t\t\t\treturn !front\n\t\t\t}\n\t\t\tif !objs[j].IsDir() {\n\t\t\t\treturn front\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n}\n\nfunc WrapObjName(objs Obj) Obj {\n\treturn &ObjWrapName{Name: utils.MappingName(objs.GetName()), Obj: objs}\n}\n\nfunc WrapObjsName(objs []Obj) {\n\tfor i := range objs {\n\t\tobjs[i] = &ObjWrapName{Name: utils.MappingName(objs[i].GetName()), Obj: objs[i]}\n\t}\n}\n\nfunc UnwrapObjName(obj Obj) Obj {\n\tif n, ok := obj.(*ObjWrapName); ok {\n\t\treturn n.Obj\n\t}\n\treturn obj\n}\n\nfunc GetThumb(obj Obj) (thumb string, ok bool) {\n\tfor {\n\t\tswitch o := obj.(type) {\n\t\tcase Thumb:\n\t\t\treturn o.Thumb(), true\n\t\tcase ObjUnwrap:\n\t\t\tobj = o.Unwrap()\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc GetUrl(obj Obj) (url string, ok bool) {\n\tfor {\n\t\tswitch o := obj.(type) {\n\t\tcase URL:\n\t\t\treturn o.URL(), true\n\t\tcase ObjUnwrap:\n\t\t\tobj = o.Unwrap()\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc GetProvider(obj Obj) (string, bool) {\n\tfor {\n\t\tswitch o := obj.(type) {\n\t\tcase ObjWithProvider:\n\t\t\treturn o.GetProvider(), true\n\t\tcase ObjUnwrap:\n\t\t\tobj = o.Unwrap()\n\t\tdefault:\n\t\t\treturn \"unknown\", false\n\t\t}\n\t}\n}\n\n// Merge\nfunc NewObjMerge() *ObjMerge {\n\treturn &ObjMerge{\n\t\tset: mapset.NewSet[string](),\n\t}\n}\n\ntype ObjMerge struct {\n\tregs []*regexp2.Regexp\n\tset  mapset.Set[string]\n}\n\nfunc (om *ObjMerge) Merge(objs []Obj, objs_ ...Obj) []Obj {\n\tnewObjs := make([]Obj, 0, len(objs)+len(objs_))\n\tnewObjs = om.insertObjs(om.insertObjs(newObjs, objs...), objs_...)\n\treturn newObjs\n}\n\nfunc (om *ObjMerge) insertObjs(objs []Obj, objs_ ...Obj) []Obj {\n\tfor _, obj := range objs_ {\n\t\tif om.clickObj(obj) {\n\t\t\tobjs = append(objs, obj)\n\t\t}\n\t}\n\treturn objs\n}\n\nfunc (om *ObjMerge) clickObj(obj Obj) bool {\n\tfor _, reg := range om.regs {\n\t\tif isMatch, _ := reg.MatchString(obj.GetName()); isMatch {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn om.set.Add(obj.GetName())\n}\n\nfunc (om *ObjMerge) InitHideReg(hides string) {\n\trs := strings.Split(hides, \"\\n\")\n\tom.regs = make([]*regexp2.Regexp, 0, len(rs))\n\tfor _, r := range rs {\n\t\tom.regs = append(om.regs, regexp2.MustCompile(r, regexp2.None))\n\t}\n}\n\nfunc (om *ObjMerge) Reset() {\n\tom.set.Clear()\n}\n\ntype ObjMask uint8\n\nfunc (m ObjMask) GetObjMask() ObjMask {\n\treturn m\n}\n\nconst (\n\tVirtual ObjMask = 1 << iota\n\tNoRename\n\tNoRemove\n\tNoMove\n\tNoCopy\n\tNoWrite\n\tTemp\n)\nconst (\n\tLocked   = NoRename | NoRemove | NoMove\n\tReadOnly = Locked | NoWrite // NoRename | NoDelete | NoMove | NoWrite\n)\n\ntype ObjWrapMask struct {\n\tObj\n\tMask ObjMask\n}\n\nfunc (m *ObjWrapMask) Unwrap() Obj {\n\treturn m.Obj\n}\nfunc (m *ObjWrapMask) GetObjMask() ObjMask {\n\treturn m.Mask\n}\n\nfunc GetObjMask(obj Obj) ObjMask {\n\tfor {\n\t\tswitch o := obj.(type) {\n\t\tcase interface{ GetObjMask() ObjMask }:\n\t\t\treturn o.GetObjMask()\n\t\tcase ObjUnwrap:\n\t\t\tobj = o.Unwrap()\n\t\tdefault:\n\t\t\treturn 0\n\t\t}\n\t}\n}\n\nfunc ObjHasMask(obj Obj, mask ObjMask) bool {\n\treturn GetObjMask(obj)&mask != 0\n}\n"
  },
  {
    "path": "internal/model/object.go",
    "content": "package model\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype ObjWrapName struct {\n\tName string\n\tObj\n}\n\nfunc (o *ObjWrapName) Unwrap() Obj {\n\treturn o.Obj\n}\n\nfunc (o *ObjWrapName) GetName() string {\n\treturn o.Name\n}\n\ntype Object struct {\n\tID       string\n\tPath     string\n\tName     string\n\tSize     int64\n\tModified time.Time\n\tCtime    time.Time // file create time\n\tIsFolder bool\n\tHashInfo utils.HashInfo\n\tMask     ObjMask\n}\n\nfunc (o *Object) GetName() string {\n\treturn o.Name\n}\n\nfunc (o *Object) GetSize() int64 {\n\treturn o.Size\n}\n\nfunc (o *Object) ModTime() time.Time {\n\treturn o.Modified\n}\nfunc (o *Object) CreateTime() time.Time {\n\tif o.Ctime.IsZero() {\n\t\treturn o.ModTime()\n\t}\n\treturn o.Ctime\n}\n\nfunc (o *Object) IsDir() bool {\n\treturn o.IsFolder\n}\n\nfunc (o *Object) GetID() string {\n\treturn o.ID\n}\n\nfunc (o *Object) GetPath() string {\n\treturn o.Path\n}\n\nfunc (o *Object) SetPath(path string) {\n\to.Path = path\n}\n\nfunc (o *Object) GetHash() utils.HashInfo {\n\treturn o.HashInfo\n}\n\nfunc (o *Object) GetObjMask() ObjMask {\n\treturn o.Mask\n}\n\ntype Thumbnail struct {\n\tThumbnail string\n}\n\ntype Url struct {\n\tUrl string\n}\n\nfunc (w Url) URL() string {\n\treturn w.Url\n}\n\nfunc (t Thumbnail) Thumb() string {\n\treturn t.Thumbnail\n}\n\ntype ObjThumb struct {\n\tObject\n\tThumbnail\n}\n\ntype ObjectURL struct {\n\tObject\n\tUrl\n}\n\ntype ObjThumbURL struct {\n\tObject\n\tThumbnail\n\tUrl\n}\n\ntype Provider struct {\n\tProvider string\n}\n\nfunc (p Provider) GetProvider() string {\n\treturn p.Provider\n}\n\ntype ObjectProvider struct {\n\tObject\n\tProvider\n}\n"
  },
  {
    "path": "internal/model/req.go",
    "content": "package model\n\ntype PageReq struct {\n\tPage    int `json:\"page\" form:\"page\"`\n\tPerPage int `json:\"per_page\" form:\"per_page\"`\n}\n\nconst MaxUint = ^uint(0)\nconst MinUint = 0\nconst MaxInt = int(MaxUint >> 1)\nconst MinInt = -MaxInt - 1\n\nfunc (p *PageReq) Validate() {\n\tif p.Page < 1 {\n\t\tp.Page = 1\n\t}\n\tif p.PerPage < 1 {\n\t\tp.PerPage = MaxInt\n\t}\n}\n"
  },
  {
    "path": "internal/model/search.go",
    "content": "package model\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype IndexProgress struct {\n\tObjCount     uint64     `json:\"obj_count\"`\n\tIsDone       bool       `json:\"is_done\"`\n\tLastDoneTime *time.Time `json:\"last_done_time\"`\n\tError        string     `json:\"error\"`\n}\n\ntype SearchReq struct {\n\tParent   string `json:\"parent\"`\n\tKeywords string `json:\"keywords\"`\n\t// 0 for all, 1 for dir, 2 for file\n\tScope int `json:\"scope\"`\n\tPageReq\n}\n\ntype SearchNode struct {\n\tParent string `json:\"parent\" gorm:\"index\"`\n\tName   string `json:\"name\"`\n\tIsDir  bool   `json:\"is_dir\"`\n\tSize   int64  `json:\"size\"`\n}\n\nfunc (p *SearchReq) Validate() error {\n\tif p.Page < 1 {\n\t\treturn fmt.Errorf(\"page can't < 1\")\n\t}\n\tif p.PerPage < 1 {\n\t\treturn fmt.Errorf(\"per_page can't < 1\")\n\t}\n\treturn nil\n}\n\nfunc (s *SearchNode) Type() string {\n\treturn \"SearchNode\"\n}\n"
  },
  {
    "path": "internal/model/setting.go",
    "content": "package model\n\nconst (\n\tSINGLE = iota\n\tSITE\n\tSTYLE\n\tPREVIEW\n\tGLOBAL\n\tOFFLINE_DOWNLOAD\n\tINDEX\n\tSSO\n\tLDAP\n\tS3\n\tFTP\n\tTRAFFIC\n)\n\nconst (\n\tPUBLIC = iota\n\tPRIVATE\n\tREADONLY\n\tDEPRECATED\n)\n\ntype SettingItem struct {\n\tKey            string `json:\"key\" gorm:\"primaryKey\" binding:\"required\"` // unique key\n\tValue          string `json:\"value\"`                                    // value\n\tMigrationValue string `json:\"-\" gorm:\"-:all\"`                           // deprecated value\n\tHelp           string `json:\"help\"`                                     // help message\n\tType           string `json:\"type\"`                                     // string, number, bool, select\n\tOptions        string `json:\"options\"`                                  // values for select\n\tGroup          int    `json:\"group\"`                                    // use to group setting in frontend\n\tFlag           int    `json:\"flag\"`                                     // 0 = public, 1 = private, 2 = readonly, 3 = deprecated, etc.\n\tIndex          uint   `json:\"index\"`\n}\n\nfunc (s SettingItem) IsDeprecated() bool {\n\treturn s.Flag == DEPRECATED\n}\n"
  },
  {
    "path": "internal/model/sharing.go",
    "content": "package model\n\nimport \"time\"\n\ntype SharingDB struct {\n\tID          string     `json:\"id\" gorm:\"type:char(12);primaryKey\"`\n\tFilesRaw    string     `json:\"-\" gorm:\"type:text\"`\n\tExpires     *time.Time `json:\"expires\"`\n\tPwd         string     `json:\"pwd\"`\n\tAccessed    int        `json:\"accessed\"`\n\tMaxAccessed int        `json:\"max_accessed\"`\n\tCreatorId   uint       `json:\"-\"`\n\tDisabled    bool       `json:\"disabled\"`\n\tRemark      string     `json:\"remark\"`\n\tReadme      string     `json:\"readme\" gorm:\"type:text\"`\n\tHeader      string     `json:\"header\" gorm:\"type:text\"`\n\tSort\n}\n\ntype Sharing struct {\n\t*SharingDB\n\tFiles   []string `json:\"files\"`\n\tCreator *User    `json:\"-\"`\n}\n\nfunc (s *Sharing) Valid() bool {\n\tif s.Disabled {\n\t\treturn false\n\t}\n\tif s.MaxAccessed > 0 && s.Accessed >= s.MaxAccessed {\n\t\treturn false\n\t}\n\tif len(s.Files) == 0 {\n\t\treturn false\n\t}\n\tif s.Creator == nil || !s.Creator.CanShare() {\n\t\treturn false\n\t}\n\tif s.Expires != nil && !s.Expires.IsZero() && s.Expires.Before(time.Now()) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (s *Sharing) Verify(pwd string) bool {\n\treturn s.Pwd == \"\" || s.Pwd == pwd\n}\n"
  },
  {
    "path": "internal/model/sshkey.go",
    "content": "package model\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n\t\"time\"\n)\n\ntype SSHPublicKey struct {\n\tID           uint      `json:\"id\" gorm:\"primaryKey\"`\n\tUserId       uint      `json:\"-\"`\n\tTitle        string    `json:\"title\"`\n\tFingerprint  string    `json:\"fingerprint\"`\n\tKeyStr       string    `gorm:\"type:text\" json:\"-\"`\n\tAddedTime    time.Time `json:\"added_time\"`\n\tLastUsedTime time.Time `json:\"last_used_time\"`\n}\n\nfunc (k *SSHPublicKey) GetKey() (ssh.PublicKey, error) {\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.KeyStr))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pubKey, nil\n}\n\nfunc (k *SSHPublicKey) UpdateLastUsedTime() {\n\tk.LastUsedTime = time.Now()\n}\n"
  },
  {
    "path": "internal/model/storage.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\ntype Storage struct {\n\tID                  uint      `json:\"id\" gorm:\"primaryKey\"`                        // unique key\n\tMountPath           string    `json:\"mount_path\" gorm:\"unique\" binding:\"required\"` // must be standardized\n\tOrder               int       `json:\"order\"`                                       // use to sort\n\tDriver              string    `json:\"driver\"`                                      // driver used\n\tCacheExpiration     int       `json:\"cache_expiration\"`                            // cache expire time\n\tCustomCachePolicies string    `json:\"custom_cache_policies\" gorm:\"type:text\"`\n\tStatus              string    `json:\"status\"`\n\tAddition            string    `json:\"addition\" gorm:\"type:text\"` // Additional information, defined in the corresponding driver\n\tRemark              string    `json:\"remark\"`\n\tModified            time.Time `json:\"modified\"`\n\tDisabled            bool      `json:\"disabled\"` // if disabled\n\tDisableIndex        bool      `json:\"disable_index\"`\n\tEnableSign          bool      `json:\"enable_sign\"`\n\tSort\n\tProxy\n}\n\ntype Sort struct {\n\tOrderBy        string `json:\"order_by\"`\n\tOrderDirection string `json:\"order_direction\"`\n\tExtractFolder  string `json:\"extract_folder\"`\n}\n\ntype Proxy struct {\n\tWebProxy     bool   `json:\"web_proxy\"`\n\tWebdavPolicy string `json:\"webdav_policy\"`\n\tProxyRange   bool   `json:\"proxy_range\"`\n\tDownProxyURL string `json:\"down_proxy_url\"`\n\t// Disable sign for DownProxyURL\n\tDisableProxySign bool `json:\"disable_proxy_sign\"`\n}\n\nfunc (s *Storage) GetStorage() *Storage {\n\treturn s\n}\n\nfunc (s *Storage) SetStorage(storage Storage) {\n\t*s = storage\n}\n\nfunc (s *Storage) SetStatus(status string) {\n\ts.Status = status\n}\n\nfunc (p Proxy) Webdav302() bool {\n\treturn p.WebdavPolicy == \"302_redirect\"\n}\n\nfunc (p Proxy) WebdavProxyURL() bool {\n\treturn p.WebdavPolicy == \"use_proxy_url\"\n}\n\ntype DiskUsage struct {\n\tTotalSpace int64\n\tUsedSpace  int64\n}\n\nfunc (d DiskUsage) FreeSpace() int64 {\n\treturn d.TotalSpace - d.UsedSpace\n}\n\nfunc (d DiskUsage) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(map[string]interface{}{\n\t\t\"total_space\": d.TotalSpace,\n\t\t\"used_space\":  d.UsedSpace,\n\t\t\"free_space\":  d.FreeSpace(),\n\t})\n}\n\ntype StorageDetails struct {\n\tDiskUsage\n}\n\ntype ObjWithStorageDetails interface {\n\tGetStorageDetails() *StorageDetails\n}\n\ntype ObjStorageDetails struct {\n\tObj\n\t*StorageDetails\n}\n\nfunc (o *ObjStorageDetails) Unwrap() Obj {\n\treturn o.Obj\n}\n\nfunc (o *ObjStorageDetails) GetStorageDetails() *StorageDetails {\n\treturn o.StorageDetails\n}\n\nfunc GetStorageDetails(obj Obj) (*StorageDetails, bool) {\n\tif obj, ok := obj.(ObjWithStorageDetails); ok {\n\t\treturn obj.GetStorageDetails(), true\n\t}\n\tif unwrap, ok := obj.(ObjUnwrap); ok {\n\t\treturn GetStorageDetails(unwrap.Unwrap())\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "internal/model/task.go",
    "content": "package model\n\ntype TaskItem struct {\n\tKey         string `json:\"key\"`\n\tPersistData string `gorm:\"type:text\" json:\"persist_data\"`\n}\n"
  },
  {
    "path": "internal/model/user.go",
    "content": "package model\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n\t\"github.com/pkg/errors\"\n)\n\nconst (\n\tGENERAL = iota\n\tGUEST   // only one exists\n\tADMIN\n)\n\nconst (\n\tStaticHashSalt = \"https://github.com/alist-org/alist\"\n\n\tInvalidUsernameOrPassword = \"Invalid username or password\"\n\tInvalid2FACode            = \"Invalid 2FA code\"\n\tTooManyAttempts           = \"Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.\"\n\tGuestCannotUpdateProfile  = \"Guest user can not update profile\"\n\tGuestCannotGenerate2FA    = \"Guest user can not generate 2FA code\"\n)\n\nvar LoginCache = cache.NewMemCache[int]()\n\nvar (\n\tDefaultLockDuration   = time.Minute * 5\n\tDefaultMaxAuthRetries = 5\n)\n\ntype User struct {\n\tID       uint   `json:\"id\" gorm:\"primaryKey\"`                      // unique key\n\tUsername string `json:\"username\" gorm:\"unique\" binding:\"required\"` // username\n\tPwdHash  string `json:\"-\"`                                         // password hash\n\tPwdTS    int64  `json:\"-\"`                                         // password timestamp\n\tSalt     string `json:\"-\"`                                         // unique salt\n\tPassword string `json:\"password\"`                                  // password\n\tBasePath string `json:\"base_path\"`                                 // base path\n\tRole     int    `json:\"role\"`                                      // user's role\n\tDisabled bool   `json:\"disabled\"`\n\t// Determine permissions by bit\n\t//   0:  can see hidden files\n\t//   1:  can access without password\n\t//   2:  can add offline download tasks\n\t//   3:  can mkdir and upload\n\t//   4:  can rename\n\t//   5:  can move\n\t//   6:  can copy\n\t//   7:  can remove\n\t//   8:  webdav read\n\t//   9:  webdav write\n\t//   10: ftp/sftp login and read\n\t//   11: ftp/sftp write\n\t//   12: can read archives\n\t//   13: can decompress archives\n\t//   14: can share\n\tPermission int32  `json:\"permission\"`\n\tOtpSecret  string `json:\"-\"`\n\tSsoID      string `json:\"sso_id\"` // unique by sso platform\n\tAuthn      string `gorm:\"type:text\" json:\"-\"`\n\tAllowLdap  bool   `json:\"allow_ldap\" gorm:\"default:true\"`\n}\n\nfunc (u *User) IsGuest() bool {\n\treturn u.Role == GUEST\n}\n\nfunc (u *User) IsAdmin() bool {\n\treturn u.Role == ADMIN\n}\n\nfunc (u *User) ValidateRawPassword(password string) error {\n\treturn u.ValidatePwdStaticHash(StaticHash(password))\n}\n\nfunc (u *User) ValidatePwdStaticHash(pwdStaticHash string) error {\n\tif pwdStaticHash == \"\" {\n\t\treturn errors.WithStack(errs.EmptyPassword)\n\t}\n\tif u.PwdHash != HashPwd(pwdStaticHash, u.Salt) {\n\t\treturn errors.WithStack(errs.WrongPassword)\n\t}\n\treturn nil\n}\n\nfunc (u *User) SetPassword(pwd string) *User {\n\tu.Salt = random.String(16)\n\tu.PwdHash = TwoHashPwd(pwd, u.Salt)\n\tu.PwdTS = time.Now().Unix()\n\treturn u\n}\n\nfunc CanSeeHides(permission int32) bool {\n\treturn permission&1 == 1\n}\n\nfunc (u *User) CanSeeHides() bool {\n\treturn CanSeeHides(u.Permission)\n}\n\nfunc CanAccessWithoutPassword(permission int32) bool {\n\treturn (permission>>1)&1 == 1\n}\n\nfunc (u *User) CanAccessWithoutPassword() bool {\n\treturn CanAccessWithoutPassword(u.Permission)\n}\n\nfunc CanAddOfflineDownloadTasks(permission int32) bool {\n\treturn (permission>>2)&1 == 1\n}\n\nfunc (u *User) CanAddOfflineDownloadTasks() bool {\n\treturn CanAddOfflineDownloadTasks(u.Permission)\n}\n\nfunc CanWrite(permission int32) bool {\n\treturn (permission>>3)&1 == 1\n}\n\nfunc (u *User) CanWrite() bool {\n\treturn CanWrite(u.Permission)\n}\n\nfunc CanRename(permission int32) bool {\n\treturn (permission>>4)&1 == 1\n}\n\nfunc (u *User) CanRename() bool {\n\treturn CanRename(u.Permission)\n}\n\nfunc CanMove(permission int32) bool {\n\treturn (permission>>5)&1 == 1\n}\n\nfunc (u *User) CanMove() bool {\n\treturn CanMove(u.Permission)\n}\n\nfunc CanCopy(permission int32) bool {\n\treturn (permission>>6)&1 == 1\n}\n\nfunc (u *User) CanCopy() bool {\n\treturn CanCopy(u.Permission)\n}\n\nfunc CanRemove(permission int32) bool {\n\treturn (permission>>7)&1 == 1\n}\n\nfunc (u *User) CanRemove() bool {\n\treturn CanRemove(u.Permission)\n}\n\nfunc CanWebdavRead(permission int32) bool {\n\treturn (permission>>8)&1 == 1\n}\n\nfunc (u *User) CanWebdavRead() bool {\n\treturn CanWebdavRead(u.Permission)\n}\n\nfunc CanWebdavManage(permission int32) bool {\n\treturn (permission>>9)&1 == 1\n}\n\nfunc (u *User) CanWebdavManage() bool {\n\treturn CanWebdavManage(u.Permission)\n}\n\nfunc CanFTPAccess(permission int32) bool {\n\treturn (permission>>10)&1 == 1\n}\n\nfunc (u *User) CanFTPAccess() bool {\n\treturn CanFTPAccess(u.Permission)\n}\n\nfunc CanFTPManage(permission int32) bool {\n\treturn (permission>>11)&1 == 1\n}\n\nfunc (u *User) CanFTPManage() bool {\n\treturn CanFTPManage(u.Permission)\n}\n\nfunc CanReadArchives(permission int32) bool {\n\treturn (permission>>12)&1 == 1\n}\n\nfunc (u *User) CanReadArchives() bool {\n\treturn CanReadArchives(u.Permission)\n}\n\nfunc CanDecompress(permission int32) bool {\n\treturn (permission>>13)&1 == 1\n}\n\nfunc (u *User) CanDecompress() bool {\n\treturn CanDecompress(u.Permission)\n}\n\nfunc CanShare(permission int32) bool {\n\treturn (permission>>14)&1 == 1\n}\n\nfunc (u *User) CanShare() bool {\n\treturn CanShare(u.Permission)\n}\n\nfunc (u *User) JoinPath(reqPath string) (string, error) {\n\treturn utils.JoinBasePath(u.BasePath, reqPath)\n}\n\nfunc StaticHash(password string) string {\n\treturn utils.HashData(utils.SHA256, []byte(fmt.Sprintf(\"%s-%s\", password, StaticHashSalt)))\n}\n\nfunc HashPwd(static string, salt string) string {\n\treturn utils.HashData(utils.SHA256, []byte(fmt.Sprintf(\"%s-%s\", static, salt)))\n}\n\nfunc TwoHashPwd(password string, salt string) string {\n\treturn HashPwd(StaticHash(password), salt)\n}\n\nfunc (u *User) WebAuthnID() []byte {\n\tbs := make([]byte, 8)\n\tbinary.LittleEndian.PutUint64(bs, uint64(u.ID))\n\treturn bs\n}\n\nfunc (u *User) WebAuthnName() string {\n\treturn u.Username\n}\n\nfunc (u *User) WebAuthnDisplayName() string {\n\treturn u.Username\n}\n\nfunc (u *User) WebAuthnCredentials() []webauthn.Credential {\n\tvar res []webauthn.Credential\n\terr := json.Unmarshal([]byte(u.Authn), &res)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\treturn res\n}\n\nfunc (u *User) WebAuthnIcon() string {\n\treturn \"https://res.oplist.org/logo/logo.svg\"\n}\n"
  },
  {
    "path": "internal/net/oss.go",
    "content": "package net\n\nimport \"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\nfunc NewOSSClient(endpoint, accessKeyID, accessKeySecret string, options ...oss.ClientOption) (*oss.Client, error) {\n\tclientOptions := []oss.ClientOption{oss.HTTPClient(NewHttpClient())}\n\tclientOptions = append(clientOptions, options...)\n\treturn oss.New(endpoint, accessKeyID, accessKeySecret, clientOptions...)\n}\n"
  },
  {
    "path": "internal/net/oss_test.go",
    "content": "package net\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n)\n\nfunc TestNewOSSClientUsesEnvironmentHTTPSProxy(t *testing.T) {\n\toldConf := conf.Conf\n\tconf.Conf = conf.DefaultConfig(\"data\")\n\tdefer func() {\n\t\tconf.Conf = oldConf\n\t}()\n\n\tt.Setenv(\"HTTP_PROXY\", \"\")\n\tt.Setenv(\"http_proxy\", \"\")\n\tt.Setenv(\"HTTPS_PROXY\", \"http://127.0.0.1:7890\")\n\tt.Setenv(\"https_proxy\", \"\")\n\tt.Setenv(\"NO_PROXY\", \"\")\n\tt.Setenv(\"no_proxy\", \"\")\n\n\tclient, err := NewOSSClient(\"https://oss-cn-hangzhou.aliyuncs.com\", \"test-access-key\", \"test-access-secret\")\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\n\tif client.HTTPClient == nil {\n\t\tt.Fatal(\"expected OSS client to use a custom HTTP client\")\n\t}\n\n\ttransport, ok := client.HTTPClient.Transport.(*http.Transport)\n\tif !ok {\n\t\tt.Fatalf(\"expected *http.Transport, got %T\", client.HTTPClient.Transport)\n\t}\n\n\tif transport.Proxy == nil {\n\t\tt.Fatal(\"expected proxy function to be configured\")\n\t}\n\n\treq := &http.Request{URL: &url.URL{Scheme: \"https\", Host: \"oss-cn-hangzhou.aliyuncs.com\"}}\n\tproxyURL, err := transport.Proxy(req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no proxy lookup error, got %v\", err)\n\t}\n\tif proxyURL == nil {\n\t\tt.Fatal(\"expected HTTPS proxy to be used\")\n\t}\n\tif got, want := proxyURL.String(), \"http://127.0.0.1:7890\"; got != want {\n\t\tt.Fatalf(\"expected proxy %q, got %q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "internal/net/request.go",
    "content": "package net\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/rclone/rclone/lib/mmap\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/aws/aws-sdk-go/aws/awsutil\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// DefaultDownloadPartSize is the default range of bytes to get at a time when\n// using Download().\nconst DefaultDownloadPartSize = utils.MB * 8\n\n// DefaultDownloadConcurrency is the default number of goroutines to spin up\n// when using Download().\nconst DefaultDownloadConcurrency = 2\n\n// DefaultPartBodyMaxRetries is the default number of retries to make when a part fails to download.\nconst DefaultPartBodyMaxRetries = 3\n\nvar DefaultConcurrencyLimit *ConcurrencyLimit\n\ntype Downloader struct {\n\tPartSize int\n\n\t// PartBodyMaxRetries is the number of retry attempts to make for failed part downloads.\n\tPartBodyMaxRetries int\n\n\t// The number of goroutines to spin up in parallel when sending parts.\n\t// If this is set to zero, the DefaultDownloadConcurrency value will be used.\n\t//\n\t// Concurrency of 1 will download the parts sequentially.\n\tConcurrency int\n\n\t//RequestParam        HttpRequestParams\n\tHttpClient HttpRequestFunc\n\n\t*ConcurrencyLimit\n}\ntype HttpRequestFunc func(ctx context.Context, params *HttpRequestParams) (*http.Response, error)\n\nfunc NewDownloader(options ...func(*Downloader)) *Downloader {\n\td := &Downloader{ //允许不设置的选项\n\t\tPartBodyMaxRetries: DefaultPartBodyMaxRetries,\n\t\tConcurrencyLimit:   DefaultConcurrencyLimit,\n\t}\n\tfor _, option := range options {\n\t\toption(d)\n\t}\n\treturn d\n}\n\n// Download The Downloader makes multi-thread http requests to remote URL, each chunk(except last one) has PartSize,\n// cache some data, then return Reader with assembled data\n// Supports range, do not support unknown FileSize, and will fail if FileSize is incorrect\n// memory usage is at about Concurrency*PartSize, use this wisely\nfunc (d Downloader) Download(ctx context.Context, p *HttpRequestParams) (readCloser io.ReadCloser, err error) {\n\n\tvar finalP HttpRequestParams\n\tawsutil.Copy(&finalP, p)\n\tif finalP.Range.Length < 0 || finalP.Range.Start+finalP.Range.Length > finalP.Size {\n\t\tfinalP.Range.Length = finalP.Size - finalP.Range.Start\n\t}\n\timpl := downloader{params: &finalP, cfg: d, ctx: ctx}\n\n\t// Ensures we don't need nil checks later on\n\t// 必需的选项\n\tif impl.cfg.Concurrency == 0 {\n\t\timpl.cfg.Concurrency = DefaultDownloadConcurrency\n\t}\n\tif impl.cfg.PartSize == 0 {\n\t\timpl.cfg.PartSize = DefaultDownloadPartSize\n\t}\n\tif conf.MaxBufferLimit > 0 && impl.cfg.PartSize > conf.MaxBufferLimit {\n\t\timpl.cfg.PartSize = conf.MaxBufferLimit\n\t}\n\tif impl.cfg.HttpClient == nil {\n\t\timpl.cfg.HttpClient = DefaultHttpRequestFunc\n\t}\n\n\treturn impl.download()\n}\n\n// downloader is the implementation structure used internally by Downloader.\ntype downloader struct {\n\tctx    context.Context\n\tcancel context.CancelCauseFunc\n\tcfg    Downloader\n\n\tparams       *HttpRequestParams //http request params\n\tchunkChannel chan chunk         //chunk chanel\n\n\t//wg sync.WaitGroup\n\tm sync.Mutex\n\n\tnextChunk int //next chunk id\n\tbufs      []*Buf\n\twritten   int64 //total bytes of file downloaded from remote\n\terr       error\n\n\tconcurrency int //剩余的并发数，递减。到0时停止并发\n\tmaxPart     int //有多少个分片\n\tpos         int64\n\tmaxPos      int64\n\tm2          sync.Mutex\n\treadingID   int // 正在被读取的id\n}\n\ntype ConcurrencyLimit struct {\n\t_m    sync.Mutex\n\tLimit int // 需要大于0\n}\n\nvar ErrExceedMaxConcurrency = HttpStatusCodeError(http.StatusTooManyRequests)\n\nfunc (l *ConcurrencyLimit) sub() error {\n\tl._m.Lock()\n\tdefer l._m.Unlock()\n\tif l.Limit-1 < 0 {\n\t\treturn ErrExceedMaxConcurrency\n\t}\n\tl.Limit--\n\t// log.Debugf(\"ConcurrencyLimit.sub: %d\", l.Limit)\n\treturn nil\n}\nfunc (l *ConcurrencyLimit) add() {\n\tl._m.Lock()\n\tdefer l._m.Unlock()\n\tl.Limit++\n\t// log.Debugf(\"ConcurrencyLimit.add: %d\", l.Limit)\n}\n\n// 检测是否超过限制\nfunc (d *downloader) concurrencyCheck() error {\n\tif d.cfg.ConcurrencyLimit != nil {\n\t\treturn d.cfg.ConcurrencyLimit.sub()\n\t}\n\treturn nil\n}\nfunc (d *downloader) concurrencyFinish() {\n\tif d.cfg.ConcurrencyLimit != nil {\n\t\td.cfg.ConcurrencyLimit.add()\n\t}\n}\n\n// download performs the implementation of the object download across ranged GETs.\nfunc (d *downloader) download() (io.ReadCloser, error) {\n\tif err := d.concurrencyCheck(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaxPart := 1\n\tif d.params.Range.Length > int64(d.cfg.PartSize) {\n\t\tmaxPart = int((d.params.Range.Length + int64(d.cfg.PartSize) - 1) / int64(d.cfg.PartSize))\n\t}\n\tif maxPart < d.cfg.Concurrency {\n\t\td.cfg.Concurrency = maxPart\n\t}\n\tlog.Debugf(\"cfgConcurrency:%d\", d.cfg.Concurrency)\n\n\tif maxPart == 1 {\n\t\tresp, err := d.cfg.HttpClient(d.ctx, d.params)\n\t\tif err != nil {\n\t\t\td.concurrencyFinish()\n\t\t\treturn nil, err\n\t\t}\n\t\tcloseFunc := resp.Body.Close\n\t\tresp.Body = utils.NewReadCloser(resp.Body, func() error {\n\t\t\td.m.Lock()\n\t\t\tdefer d.m.Unlock()\n\t\t\tvar err error\n\t\t\tif closeFunc != nil {\n\t\t\t\td.concurrencyFinish()\n\t\t\t\terr = closeFunc()\n\t\t\t\tcloseFunc = nil\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t\treturn resp.Body, nil\n\t}\n\td.ctx, d.cancel = context.WithCancelCause(d.ctx)\n\n\t// workers\n\td.chunkChannel = make(chan chunk, d.cfg.Concurrency)\n\n\td.maxPart = maxPart\n\td.pos = d.params.Range.Start\n\td.maxPos = d.params.Range.Start + d.params.Range.Length\n\td.concurrency = d.cfg.Concurrency\n\t_ = d.sendChunkTask(true)\n\n\tvar rc io.ReadCloser = NewMultiReadCloser(d.bufs[0], d.interrupt, d.finishBuf)\n\n\t// Return error\n\treturn rc, d.err\n}\n\nfunc (d *downloader) sendChunkTask(newConcurrency bool) error {\n\td.m.Lock()\n\tdefer d.m.Unlock()\n\tisNewBuf := d.concurrency > 0\n\tif newConcurrency {\n\t\tif d.concurrency <= 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif d.nextChunk > 0 { // 第一个不检查，因为已经检查过了\n\t\t\tif err := d.concurrencyCheck(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\td.concurrency--\n\t\tgo d.downloadPart()\n\t}\n\n\tvar buf *Buf\n\tif isNewBuf {\n\t\tbuf = NewBuf(d.ctx, d.cfg.PartSize)\n\t\td.bufs = append(d.bufs, buf)\n\t} else {\n\t\tbuf = d.getBuf(d.nextChunk)\n\t}\n\n\tif d.pos < d.maxPos {\n\t\tfinalSize := int64(d.cfg.PartSize)\n\t\tswitch d.nextChunk {\n\t\tcase 0:\n\t\t\t// 最小分片在前面有助视频播放？\n\t\t\tfirstSize := d.params.Range.Length % finalSize\n\t\t\tif firstSize > 0 {\n\t\t\t\tminSize := finalSize / 2\n\t\t\t\tif firstSize < minSize { // 最小分片太小就调整到一半\n\t\t\t\t\tfinalSize = minSize\n\t\t\t\t} else {\n\t\t\t\t\tfinalSize = firstSize\n\t\t\t\t}\n\t\t\t}\n\t\tcase 1:\n\t\t\tfirstSize := d.params.Range.Length % finalSize\n\t\t\tminSize := finalSize / 2\n\t\t\tif firstSize > 0 && firstSize < minSize {\n\t\t\t\tfinalSize += firstSize - minSize\n\t\t\t}\n\t\t}\n\t\terr := buf.Reset(int(finalSize))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tch := chunk{\n\t\t\tstart: d.pos,\n\t\t\tsize:  finalSize,\n\t\t\tid:    d.nextChunk,\n\t\t\tbuf:   buf,\n\n\t\t\tnewConcurrency: newConcurrency,\n\t\t}\n\t\td.pos += finalSize\n\t\td.nextChunk++\n\t\td.chunkChannel <- ch\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// when the final reader Close, we interrupt\nfunc (d *downloader) interrupt() error {\n\td.m.Lock()\n\tdefer d.m.Unlock()\n\terr := d.err\n\tif err == nil && d.written != d.params.Range.Length {\n\t\tlog.Debugf(\"Downloader interrupt before finish\")\n\t\terr := fmt.Errorf(\"interrupted\")\n\t\td.err = err\n\t}\n\tclose(d.chunkChannel)\n\tif d.bufs != nil {\n\t\td.cancel(err)\n\t\tfor _, buf := range d.bufs {\n\t\t\tbuf.Close()\n\t\t}\n\t\td.bufs = nil\n\t\tif d.concurrency > 0 {\n\t\t\td.concurrency = -d.concurrency\n\t\t}\n\t\tlog.Debugf(\"maxConcurrency:%d\", d.cfg.Concurrency+d.concurrency)\n\t}\n\treturn err\n}\nfunc (d *downloader) getBuf(id int) (b *Buf) {\n\treturn d.bufs[id%len(d.bufs)]\n}\nfunc (d *downloader) finishBuf(id int) (isLast bool, nextBuf *Buf) {\n\tid++\n\tif id >= d.maxPart {\n\t\treturn true, nil\n\t}\n\n\t_ = d.sendChunkTask(false)\n\n\td.readingID = id\n\treturn false, d.getBuf(id)\n}\n\n// downloadPart is an individual goroutine worker reading from the ch channel\n// and performing Http request on the data with a given byte range.\nfunc (d *downloader) downloadPart() {\n\tdefer d.concurrencyFinish()\n\tfor {\n\t\tselect {\n\t\tcase <-d.ctx.Done():\n\t\t\treturn\n\t\tcase c, ok := <-d.chunkChannel:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.getErr() != nil {\n\t\t\t\t// Drain the channel if there is an error, to prevent deadlocking\n\t\t\t\t// of download producer.\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := d.downloadChunk(&c); err != nil {\n\t\t\t\tif err == errCancelConcurrency {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err == context.Canceled {\n\t\t\t\t\tif e := context.Cause(d.ctx); e != nil {\n\t\t\t\t\t\terr = e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\td.setErr(err)\n\t\t\t\td.cancel(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// downloadChunk downloads the chunk\nfunc (d *downloader) downloadChunk(ch *chunk) error {\n\tlog.Debugf(\"start chunk_%d, %+v\", ch.id, ch)\n\tparams := d.getParamsFromChunk(ch)\n\tvar n int64\n\tvar err error\n\tfor retry := 0; retry <= d.cfg.PartBodyMaxRetries; retry++ {\n\t\tif d.getErr() != nil {\n\t\t\treturn nil\n\t\t}\n\t\tn, err = d.tryDownloadChunk(params, ch)\n\t\tif err == nil {\n\t\t\td.incrWritten(n)\n\t\t\tlog.Debugf(\"chunk_%d downloaded\", ch.id)\n\t\t\tbreak\n\t\t}\n\t\tif d.getErr() != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif utils.IsCanceled(d.ctx) {\n\t\t\treturn d.ctx.Err()\n\t\t}\n\t\t// Check if the returned error is an errNeedRetry.\n\t\t// If this occurs we unwrap the err to set the underlying error\n\t\t// and attempt any remaining retries.\n\t\tif e, ok := err.(*errNeedRetry); ok {\n\t\t\terr = e.Unwrap()\n\t\t\tif n > 0 {\n\t\t\t\t// 测试：下载时 断开openlist向云盘发起的下载连接\n\t\t\t\t// 校验：下载完后校验文件哈希值 一致\n\t\t\t\td.incrWritten(n)\n\t\t\t\tch.start += n\n\t\t\t\tch.size -= n\n\t\t\t\tparams.Range.Start = ch.start\n\t\t\t\tparams.Range.Length = ch.size\n\t\t\t}\n\t\t\tlog.Warnf(\"err chunk_%d, object part download error %s, retrying attempt %d. %v\",\n\t\t\t\tch.id, params.URL, retry, err)\n\t\t} else if err == errInfiniteRetry {\n\t\t\tretry--\n\t\t\tcontinue\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn err\n}\n\nvar errCancelConcurrency = errors.New(\"cancel concurrency\")\nvar errInfiniteRetry = errors.New(\"infinite retry\")\n\nfunc (d *downloader) tryDownloadChunk(params *HttpRequestParams, ch *chunk) (int64, error) {\n\tresp, err := d.cfg.HttpClient(d.ctx, params)\n\tif err != nil {\n\t\tstatusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError)\n\t\tif !ok {\n\t\t\treturn 0, err\n\t\t}\n\t\tif statusCode == http.StatusRequestedRangeNotSatisfiable {\n\t\t\treturn 0, err\n\t\t}\n\t\tif ch.id == 0 { //第1个任务 有限的重试，超过重试就会结束请求\n\t\t\tswitch statusCode {\n\t\t\tdefault:\n\t\t\t\treturn 0, err\n\t\t\tcase http.StatusTooManyRequests:\n\t\t\tcase http.StatusBadGateway:\n\t\t\tcase http.StatusServiceUnavailable:\n\t\t\tcase http.StatusGatewayTimeout:\n\t\t\t}\n\t\t\t<-time.After(time.Millisecond * 200)\n\t\t\treturn 0, &errNeedRetry{err: err}\n\t\t}\n\n\t\t// 来到这 说明第1个分片下载 连接成功了\n\t\t// 后续分片下载出错都当超载处理\n\t\tlog.Debugf(\"err chunk_%d, try downloading:%v\", ch.id, err)\n\n\t\td.m.Lock()\n\t\tisCancelConcurrency := ch.newConcurrency\n\t\tif d.concurrency > 0 { // 取消剩余的并发任务\n\t\t\t// 用于计算实际的并发数\n\t\t\td.concurrency = -d.concurrency\n\t\t\tisCancelConcurrency = true\n\t\t}\n\t\tif isCancelConcurrency {\n\t\t\td.concurrency--\n\t\t\td.chunkChannel <- *ch\n\t\t\td.m.Unlock()\n\t\t\treturn 0, errCancelConcurrency\n\t\t}\n\t\td.m.Unlock()\n\t\tif ch.id != d.readingID { //正在被读取的优先重试\n\t\t\td.m2.Lock()\n\t\t\tdefer d.m2.Unlock()\n\t\t\t<-time.After(time.Millisecond * 200)\n\t\t}\n\t\treturn 0, errInfiniteRetry\n\t}\n\tdefer resp.Body.Close()\n\t//only check file size on the first task\n\tif ch.id == 0 {\n\t\terr = d.checkTotalBytes(resp)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\t_ = d.sendChunkTask(true)\n\tn, err := utils.CopyWithBuffer(ch.buf, resp.Body)\n\n\tif err != nil {\n\t\treturn n, &errNeedRetry{err: err}\n\t}\n\tif n != ch.size {\n\t\terr = fmt.Errorf(\"chunk download size incorrect, expected=%d, got=%d\", ch.size, n)\n\t\treturn n, &errNeedRetry{err: err}\n\t}\n\n\treturn n, nil\n}\nfunc (d *downloader) getParamsFromChunk(ch *chunk) *HttpRequestParams {\n\tvar params HttpRequestParams\n\tawsutil.Copy(&params, d.params)\n\n\t// Get the getBuf byte range of data\n\tparams.Range = http_range.Range{Start: ch.start, Length: ch.size}\n\treturn &params\n}\n\nfunc (d *downloader) checkTotalBytes(resp *http.Response) error {\n\tvar err error\n\ttotalBytes := int64(-1)\n\tcontentRange := resp.Header.Get(\"Content-Range\")\n\tif len(contentRange) == 0 {\n\t\t// ContentRange is nil when the full file contents is provided, and\n\t\t// is not chunked. Use ContentLength instead.\n\t\tif resp.ContentLength > 0 {\n\t\t\ttotalBytes = resp.ContentLength\n\t\t}\n\t} else {\n\t\tparts := strings.Split(contentRange, \"/\")\n\n\t\ttotal := int64(-1)\n\n\t\t// Checking for whether a numbered total exists\n\t\t// If one does not exist, we will assume the total to be -1, undefined,\n\t\t// and sequentially download each chunk until hitting a 416 error\n\t\ttotalStr := parts[len(parts)-1]\n\t\tif totalStr != \"*\" {\n\t\t\ttotal, err = strconv.ParseInt(totalStr, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"failed extracting file size\")\n\t\t\t}\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"file size unknown\")\n\t\t}\n\n\t\ttotalBytes = total\n\t}\n\tif totalBytes != d.params.Size && err == nil {\n\t\terr = fmt.Errorf(\"expect file size=%d unmatch remote report size=%d, need refresh cache\", d.params.Size, totalBytes)\n\t}\n\tif err != nil {\n\t\t// _ = d.interrupt()\n\t\td.setErr(err)\n\t\td.cancel(err)\n\t}\n\treturn err\n\n}\n\nfunc (d *downloader) incrWritten(n int64) {\n\td.m.Lock()\n\tdefer d.m.Unlock()\n\n\td.written += n\n}\n\n// getErr is a thread-safe getter for the error object\nfunc (d *downloader) getErr() error {\n\td.m.Lock()\n\tdefer d.m.Unlock()\n\n\treturn d.err\n}\n\n// setErr is a thread-safe setter for the error object\nfunc (d *downloader) setErr(e error) {\n\td.m.Lock()\n\tdefer d.m.Unlock()\n\n\td.err = e\n}\n\n// Chunk represents a single chunk of data to write by the worker routine.\n// This structure also implements an io.SectionReader style interface for\n// io.WriterAt, effectively making it an io.SectionWriter (which does not\n// exist).\ntype chunk struct {\n\tstart int64\n\tsize  int64\n\tbuf   *Buf\n\tid    int\n\n\tnewConcurrency bool\n}\n\nfunc DefaultHttpRequestFunc(ctx context.Context, params *HttpRequestParams) (*http.Response, error) {\n\theader := http_range.ApplyRangeToHttpHeader(params.Range, params.HeaderRef)\n\treturn RequestHttp(ctx, \"GET\", header, params.URL)\n}\n\nfunc GetRangeReaderHttpRequestFunc(rangeReader model.RangeReaderIF) HttpRequestFunc {\n\treturn func(ctx context.Context, params *HttpRequestParams) (*http.Response, error) {\n\t\trc, err := rangeReader.RangeRead(ctx, params.Range)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &http.Response{\n\t\t\tStatusCode: http.StatusPartialContent,\n\t\t\tStatus:     http.StatusText(http.StatusPartialContent),\n\t\t\tBody:       rc,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Content-Range\": {params.Range.ContentRange(params.Size)},\n\t\t\t},\n\t\t\tContentLength: params.Range.Length,\n\t\t}, nil\n\t}\n}\n\ntype HttpRequestParams struct {\n\tURL string\n\t//only want data within this range\n\tRange     http_range.Range\n\tHeaderRef http.Header\n\t//total file size\n\tSize int64\n}\ntype errNeedRetry struct {\n\terr error\n}\n\nfunc (e *errNeedRetry) Error() string {\n\treturn e.err.Error()\n}\n\nfunc (e *errNeedRetry) Unwrap() error {\n\treturn e.err\n}\n\ntype MultiReadCloser struct {\n\tcfg    *cfg\n\tcloser closerFunc\n\tfinish finishBufFUnc\n}\n\ntype cfg struct {\n\trPos   int //current reader position, start from 0\n\tcurBuf *Buf\n}\n\ntype closerFunc func() error\ntype finishBufFUnc func(id int) (isLast bool, buf *Buf)\n\n// NewMultiReadCloser to save memory, we re-use limited Buf, and feed data to Read()\nfunc NewMultiReadCloser(buf *Buf, c closerFunc, fb finishBufFUnc) *MultiReadCloser {\n\treturn &MultiReadCloser{closer: c, finish: fb, cfg: &cfg{curBuf: buf}}\n}\n\nfunc (mr MultiReadCloser) Read(p []byte) (n int, err error) {\n\tif mr.cfg.curBuf == nil {\n\t\treturn 0, io.EOF\n\t}\n\tn, err = mr.cfg.curBuf.Read(p)\n\t//log.Debugf(\"read_%d read current buffer, n=%d ,err=%+v\", mr.cfg.rPos, n, err)\n\tif err == io.EOF {\n\t\tlog.Debugf(\"read_%d finished current buffer\", mr.cfg.rPos)\n\n\t\tisLast, next := mr.finish(mr.cfg.rPos)\n\t\tif isLast {\n\t\t\treturn n, io.EOF\n\t\t}\n\t\tmr.cfg.curBuf = next\n\t\tmr.cfg.rPos++\n\t\treturn n, nil\n\t}\n\tif err == context.Canceled {\n\t\tif e := context.Cause(mr.cfg.curBuf.ctx); e != nil {\n\t\t\terr = e\n\t\t}\n\t}\n\treturn n, err\n}\nfunc (mr MultiReadCloser) Close() error {\n\treturn mr.closer()\n}\n\ntype Buf struct {\n\tsize int //expected size\n\tctx  context.Context\n\toffR int\n\toffW int\n\trw   sync.Mutex\n\tbuf  []byte\n\tmmap bool\n\n\treadSignal  chan struct{}\n\treadPending bool\n}\n\n// NewBuf is a buffer that can have 1 read & 1 write at the same time.\n// when read is faster write, immediately feed data to read after written\nfunc NewBuf(ctx context.Context, maxSize int) *Buf {\n\tbr := &Buf{\n\t\tctx:        ctx,\n\t\tsize:       maxSize,\n\t\treadSignal: make(chan struct{}, 1),\n\t}\n\tif conf.MmapThreshold > 0 && maxSize >= conf.MmapThreshold {\n\t\tm, err := mmap.Alloc(maxSize)\n\t\tif err == nil {\n\t\t\tbr.buf = m\n\t\t\tbr.mmap = true\n\t\t\treturn br\n\t\t}\n\t}\n\tbr.buf = make([]byte, maxSize)\n\treturn br\n}\n\nfunc (br *Buf) Reset(size int) error {\n\tbr.rw.Lock()\n\tdefer br.rw.Unlock()\n\tif br.buf == nil {\n\t\treturn io.ErrClosedPipe\n\t}\n\tif size > cap(br.buf) {\n\t\treturn fmt.Errorf(\"reset size %d exceeds max size %d\", size, cap(br.buf))\n\t}\n\tbr.size = size\n\tbr.offR = 0\n\tbr.offW = 0\n\treturn nil\n}\n\nfunc (br *Buf) Read(p []byte) (int, error) {\n\tif err := br.ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\tif br.offR >= br.size {\n\t\treturn 0, io.EOF\n\t}\n\tfor {\n\t\tbr.rw.Lock()\n\t\tif br.buf == nil {\n\t\t\tbr.rw.Unlock()\n\t\t\treturn 0, io.ErrClosedPipe\n\t\t}\n\n\t\tif br.offW < br.offR {\n\t\t\tbr.rw.Unlock()\n\t\t\treturn 0, io.ErrUnexpectedEOF\n\t\t}\n\t\tif br.offW == br.offR {\n\t\t\tbr.readPending = true\n\t\t\tbr.rw.Unlock()\n\t\t\tselect {\n\t\t\tcase <-br.ctx.Done():\n\t\t\t\treturn 0, br.ctx.Err()\n\t\t\tcase _, ok := <-br.readSignal:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn 0, io.ErrClosedPipe\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tn := copy(p, br.buf[br.offR:br.offW])\n\t\tbr.offR += n\n\t\tbr.rw.Unlock()\n\t\tif n < len(p) && br.offR >= br.size {\n\t\t\treturn n, io.EOF\n\t\t}\n\t\treturn n, nil\n\t}\n}\n\nfunc (br *Buf) Write(p []byte) (int, error) {\n\tif err := br.ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\tbr.rw.Lock()\n\tdefer br.rw.Unlock()\n\tif br.buf == nil {\n\t\treturn 0, io.ErrClosedPipe\n\t}\n\tif br.offW >= br.size {\n\t\treturn 0, io.ErrShortWrite\n\t}\n\tn := copy(br.buf[br.offW:], p[:min(br.size-br.offW, len(p))])\n\tbr.offW += n\n\tif br.readPending {\n\t\tbr.readPending = false\n\t\tselect {\n\t\tcase br.readSignal <- struct{}{}:\n\t\tdefault:\n\t\t}\n\t}\n\tif n < len(p) {\n\t\treturn n, io.ErrShortWrite\n\t}\n\treturn n, nil\n}\n\nfunc (br *Buf) Close() error {\n\tbr.rw.Lock()\n\tdefer br.rw.Unlock()\n\tvar err error\n\tif br.mmap {\n\t\terr = mmap.Free(br.buf)\n\t\tbr.mmap = false\n\t}\n\tbr.buf = nil\n\tclose(br.readSignal)\n\treturn err\n}\n"
  },
  {
    "path": "internal/net/request_test.go",
    "content": "package net\n\n//no http range\n//\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar buf22MB = make([]byte, 1024*1024*22)\n\nfunc containsString(slice []string, val string) bool {\n\tfor _, item := range slice {\n\t\tif item == val {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc dummyHttpRequest(data []byte, p http_range.Range) io.ReadCloser {\n\n\tend := p.Start + p.Length - 1\n\n\tif end >= int64(len(data)) {\n\t\tend = int64(len(data))\n\t}\n\n\tbodyBytes := data[p.Start:end]\n\treturn io.NopCloser(bytes.NewReader(bodyBytes))\n}\n\nfunc TestDownloadOrder(t *testing.T) {\n\tbuff := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}\n\tdownloader, invocations, ranges := newDownloadRangeClient(buff)\n\tcon, partSize := 3, 3\n\td := NewDownloader(func(d *Downloader) {\n\t\td.Concurrency = con\n\t\td.PartSize = partSize\n\t\td.HttpClient = downloader.HttpRequest\n\t})\n\n\tvar start, length int64 = 2, 10\n\tlength2 := length\n\tif length2 == -1 {\n\t\tlength2 = int64(len(buff)) - start\n\t}\n\treq := &HttpRequestParams{\n\t\tRange: http_range.Range{Start: start, Length: length},\n\t\tSize:  int64(len(buff)),\n\t}\n\treadCloser, err := d.Download(context.Background(), req)\n\n\tif err != nil {\n\t\tt.Fatalf(\"expect no error, got %v\", err)\n\t}\n\tresultBuf, err := io.ReadAll(readCloser)\n\tif err != nil {\n\t\tt.Fatalf(\"expect no error, got %v\", err)\n\t}\n\tif exp, a := int(length), len(resultBuf); exp != a {\n\t\tt.Errorf(\"expect  buffer length=%d, got %d\", exp, a)\n\t}\n\tchunkSize := int(length+int64(partSize)-1) / partSize\n\tif e, a := chunkSize, *invocations; e != a {\n\t\tt.Errorf(\"expect %v API calls, got %v\", e, a)\n\t}\n\n\texpectRngs := []string{\"2-1\", \"6-3\", \"3-3\", \"9-3\"}\n\tfor _, rng := range expectRngs {\n\t\tif !containsString(*ranges, rng) {\n\t\t\tt.Errorf(\"expect range %v, but absent in return\", rng)\n\t\t}\n\t}\n\tif e, a := expectRngs, *ranges; len(e) != len(a) {\n\t\tt.Errorf(\"expect %v ranges, got %v\", e, a)\n\t}\n}\nfunc init() {\n\tFormatter := new(logrus.TextFormatter)\n\tFormatter.TimestampFormat = \"2006-01-02T15:04:05.999999999\"\n\tFormatter.FullTimestamp = true\n\tFormatter.ForceColors = true\n\tlogrus.SetFormatter(Formatter)\n\tlogrus.SetLevel(logrus.DebugLevel)\n\tlogrus.Debugf(\"Download start\")\n}\n\nfunc TestDownloadSingle(t *testing.T) {\n\tbuff := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}\n\tdownloader, invocations, ranges := newDownloadRangeClient(buff)\n\tcon, partSize := 1, 4\n\td := NewDownloader(func(d *Downloader) {\n\t\td.Concurrency = con\n\t\td.PartSize = partSize\n\t\td.HttpClient = downloader.HttpRequest\n\t})\n\n\tvar start, length int64 = 2, 10\n\treq := &HttpRequestParams{\n\t\tRange: http_range.Range{Start: start, Length: length},\n\t\tSize:  int64(len(buff)),\n\t}\n\n\treadCloser, err := d.Download(context.Background(), req)\n\n\tif err != nil {\n\t\tt.Fatalf(\"expect no error, got %v\", err)\n\t}\n\tresultBuf, err := io.ReadAll(readCloser)\n\tif err != nil {\n\t\tt.Fatalf(\"expect no error, got %v\", err)\n\t}\n\tif exp, a := int(length), len(resultBuf); exp != a {\n\t\tt.Errorf(\"expect  buffer length=%d, got %d\", exp, a)\n\t}\n\tif e, a := int(length+int64(partSize)-1)/partSize, *invocations; e != a {\n\t\tt.Errorf(\"expect %v API calls, got %v\", e, a)\n\t}\n\n\texpectRngs := []string{\"2-2\", \"4-4\", \"8-4\"}\n\tfor _, rng := range expectRngs {\n\t\tif !containsString(*ranges, rng) {\n\t\t\tt.Errorf(\"expect range %v, but absent in return\", rng)\n\t\t}\n\t}\n\tif e, a := expectRngs, *ranges; len(e) != len(a) {\n\t\tt.Errorf(\"expect %v ranges, got %v\", e, a)\n\t}\n}\n\ntype downloadCaptureClient struct {\n\tmockedHttpRequest    func(params *HttpRequestParams) (*http.Response, error)\n\tGetObjectInvocations int\n\n\tRetrievedRanges []string\n\n\tlock sync.Mutex\n}\n\nfunc (c *downloadCaptureClient) HttpRequest(ctx context.Context, params *HttpRequestParams) (*http.Response, error) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tc.GetObjectInvocations++\n\n\tif params.Range.Length != 0 {\n\t\tc.RetrievedRanges = append(c.RetrievedRanges, fmt.Sprintf(\"%d-%d\", params.Range.Start, params.Range.Length))\n\t}\n\n\treturn c.mockedHttpRequest(params)\n}\n\nfunc newDownloadRangeClient(data []byte) (*downloadCaptureClient, *int, *[]string) {\n\tcapture := &downloadCaptureClient{}\n\n\tcapture.mockedHttpRequest = func(params *HttpRequestParams) (*http.Response, error) {\n\t\tstart, fin := params.Range.Start, params.Range.Start+params.Range.Length\n\t\tif params.Range.Length == -1 || fin >= int64(len(data)) {\n\t\t\tfin = int64(len(data))\n\t\t}\n\t\tbodyBytes := data[start:fin]\n\n\t\theader := &http.Header{}\n\t\theader.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", start, fin-1, len(data)))\n\t\treturn &http.Response{\n\t\t\tBody:          io.NopCloser(bytes.NewReader(bodyBytes)),\n\t\t\tHeader:        *header,\n\t\t\tContentLength: int64(len(bodyBytes)),\n\t\t}, nil\n\t}\n\n\treturn capture, &capture.GetObjectInvocations, &capture.RetrievedRanges\n}\n"
  },
  {
    "path": "internal/net/serve.go",
    "content": "package net\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n//this file is inspired by GO_SDK net.http.ServeContent\n\n//type RangeReadCloser struct {\n//\tGetReaderForRange RangeReaderFunc\n//}\n\n// ServeHTTP replies to the request using the content in the\n// provided RangeReadCloser. The main benefit of ServeHTTP over io.Copy\n// is that it handles Range requests properly, sets the MIME type, and\n// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,\n// and If-Range requests.\n//\n// If the response's Content-Type header is not set, ServeHTTP\n// first tries to deduce the type from name's file extension and,\n// if that fails, falls back to reading the first block of the content\n// and passing it to DetectContentType.\n// The name is otherwise unused; in particular it can be empty and is\n// never sent in the response.\n//\n// If modtime is not the zero time or Unix epoch, ServeHTTP\n// includes it in a Last-Modified header in the response. If the\n// request includes an If-Modified-Since header, ServeHTTP uses\n// modtime to decide whether the content needs to be sent at all.\n//\n// The content's RangeReadCloser method must work: ServeHTTP gives a range,\n// caller will give the reader for that Range.\n//\n// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,\n// ServeHTTP uses it to handle requests using If-Match, If-None-Match, or If-Range.\nfunc ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time.Time, size int64, RangeReadCloser model.RangeReadCloserIF) error {\n\tdefer RangeReadCloser.Close()\n\tsetLastModified(w, modTime)\n\tdone, rangeReq := checkPreconditions(w, r, modTime)\n\tif done {\n\t\treturn nil\n\t}\n\n\tif size < 0 {\n\t\t// since too many functions need file size to work,\n\t\t// will not implement the support of unknown file size here\n\t\thttp.Error(w, \"negative content size not supported\", http.StatusInternalServerError)\n\t\treturn nil\n\t}\n\n\tcode := http.StatusOK\n\n\t// If Content-Type isn't set, use the file's extension to find it, but\n\t// if the Content-Type is unset explicitly, do not sniff the type.\n\tcontentTypes, haveType := w.Header()[\"Content-Type\"]\n\tvar contentType string\n\tif !haveType {\n\t\tcontentType = utils.GetMimeType(name)\n\t\tw.Header().Set(\"Content-Type\", contentType)\n\t} else if len(contentTypes) > 0 {\n\t\tcontentType = contentTypes[0]\n\t}\n\n\t// handle Content-Range header.\n\tsendSize := size\n\tvar sendContent io.ReadCloser\n\tranges, err := http_range.ParseRange(rangeReq, size)\n\tswitch {\n\tcase err == nil:\n\tcase errors.Is(err, http_range.ErrNoOverlap):\n\t\tif size == 0 {\n\t\t\t// Some clients add a Range header to all requests to\n\t\t\t// limit the size of the response. If the file is empty,\n\t\t\t// ignore the range header and respond with a 200 rather\n\t\t\t// than a 416.\n\t\t\tranges = nil\n\t\t\tbreak\n\t\t}\n\t\tw.Header().Set(\"Content-Range\", fmt.Sprintf(\"bytes */%d\", size))\n\t\tfallthrough\n\tdefault:\n\t\thttp.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)\n\t\treturn nil\n\t}\n\n\tif sumRangesSize(ranges) > size {\n\t\t// The total number of bytes in all the ranges is larger than the size of the file\n\t\t// or unknown file size, ignore the range request.\n\t\tranges = nil\n\t}\n\n\t// 使用请求的Context\n\t// 不然从sendContent读不到数据，即使请求断开CopyBuffer也会一直堵塞\n\tctx := r.Context()\n\tswitch {\n\tcase len(ranges) == 0:\n\t\treader, err := RangeReadCloser.RangeRead(ctx, http_range.Range{Length: -1})\n\t\tif err != nil {\n\t\t\tcode = http.StatusRequestedRangeNotSatisfiable\n\t\t\tif statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {\n\t\t\t\tcode = int(statusCode)\n\t\t\t}\n\t\t\thttp.Error(w, err.Error(), code)\n\t\t\treturn nil\n\t\t}\n\t\tsendContent = reader\n\tcase len(ranges) == 1:\n\t\t// RFC 7233, Section 4.1:\n\t\t// \"If a single part is being transferred, the server\n\t\t// generating the 206 response MUST generate a\n\t\t// Content-Range header field, describing what range\n\t\t// of the selected representation is enclosed, and a\n\t\t// payload consisting of the range.\n\t\t// ...\n\t\t// A server MUST NOT generate a multipart response to\n\t\t// a request for a single range, since a client that\n\t\t// does not request multiple parts might not support\n\t\t// multipart responses.\"\n\t\tra := ranges[0]\n\t\tsendContent, err = RangeReadCloser.RangeRead(ctx, ra)\n\t\tif err != nil {\n\t\t\tcode = http.StatusRequestedRangeNotSatisfiable\n\t\t\tif statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {\n\t\t\t\tcode = int(statusCode)\n\t\t\t}\n\t\t\thttp.Error(w, err.Error(), code)\n\t\t\treturn nil\n\t\t}\n\t\tsendSize = ra.Length\n\t\tcode = http.StatusPartialContent\n\t\tw.Header().Set(\"Content-Range\", ra.ContentRange(size))\n\tcase len(ranges) > 1:\n\t\tsendSize, err = rangesMIMESize(ranges, contentType, size)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)\n\t\t}\n\t\tcode = http.StatusPartialContent\n\n\t\tpr, pw := io.Pipe()\n\t\tmw := multipart.NewWriter(pw)\n\t\tw.Header().Set(\"Content-Type\", \"multipart/byteranges; boundary=\"+mw.Boundary())\n\t\tsendContent = pr\n\t\tdefer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.\n\t\tgo func() {\n\t\t\tfor _, ra := range ranges {\n\t\t\t\tpart, err := mw.CreatePart(ra.MimeHeader(contentType, size))\n\t\t\t\tif err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\treader, err := RangeReadCloser.RangeRead(ctx, ra)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif _, err := utils.CopyWithBufferN(part, reader, ra.Length); err != nil {\n\t\t\t\t\tpw.CloseWithError(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmw.Close()\n\t\t\tpw.Close()\n\t\t}()\n\t}\n\n\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\tif w.Header().Get(\"Content-Encoding\") == \"\" {\n\t\tw.Header().Set(\"Content-Length\", strconv.FormatInt(sendSize, 10))\n\t}\n\n\tw.WriteHeader(code)\n\n\tif r.Method != \"HEAD\" {\n\t\twritten, err := utils.CopyWithBufferN(w, sendContent, sendSize)\n\t\tif err != nil {\n\t\t\tif errors.Is(context.Cause(ctx), context.Canceled) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlog.Warnf(\"ServeHttp error. err: %s \", err)\n\t\t\tif written != sendSize {\n\t\t\t\tlog.Warnf(\"Maybe size incorrect or reader not giving correct/full data, or connection closed before finish. written bytes: %d ,sendSize:%d, \", written, sendSize)\n\t\t\t}\n\t\t\tcode = http.StatusInternalServerError\n\t\t\tif statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {\n\t\t\t\tcode = int(statusCode)\n\t\t\t}\n\t\t\tw.WriteHeader(code)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\nfunc ProcessHeader(origin, override http.Header) http.Header {\n\tresult := http.Header{}\n\t// client header\n\tfor h, val := range origin {\n\t\tif utils.SliceContains(conf.SlicesMap[conf.ProxyIgnoreHeaders], strings.ToLower(h)) {\n\t\t\tcontinue\n\t\t}\n\t\tresult[h] = val\n\t}\n\t// needed header\n\tfor h, val := range override {\n\t\tresult[h] = val\n\t}\n\treturn result\n}\n\n// RequestHttp deal with Header properly then send the request\nfunc RequestHttp(ctx context.Context, httpMethod string, headerOverride http.Header, URL string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, httpMethod, URL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header = headerOverride\n\tres, err := HttpClient().Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// TODO clean header with blocklist or passlist\n\tres.Header.Del(\"set-cookie\")\n\tvar reader io.Reader\n\tif res.StatusCode >= 400 {\n\t\t// 根据 Content-Encoding 判断 Body 是否压缩\n\t\tswitch res.Header.Get(\"Content-Encoding\") {\n\t\tcase \"gzip\":\n\t\t\t// 使用gzip.NewReader解压缩\n\t\t\treader, _ = gzip.NewReader(res.Body)\n\t\t\tdefer reader.(*gzip.Reader).Close()\n\t\tdefault:\n\t\t\t// 没有Content-Encoding，直接读取\n\t\t\treader = res.Body\n\t\t}\n\t\tall, _ := io.ReadAll(reader)\n\t\t_ = res.Body.Close()\n\t\tmsg := string(all)\n\t\tlog.Debugln(msg)\n\t\treturn nil, fmt.Errorf(\"http request [%s] failure,status: %w response:%s\", URL, HttpStatusCodeError(res.StatusCode), msg)\n\t}\n\treturn res, nil\n}\n\ntype HttpStatusCodeError int\n\nfunc (e HttpStatusCodeError) Error() string {\n\treturn fmt.Sprintf(\"%d|%s\", e, http.StatusText(int(e)))\n}\n\nvar once sync.Once\nvar httpClient *http.Client\n\nfunc HttpClient() *http.Client {\n\tonce.Do(func() {\n\t\thttpClient = NewHttpClient()\n\t\thttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= 10 {\n\t\t\t\treturn errors.New(\"stopped after 10 redirects\")\n\t\t\t}\n\t\t\treq.Header.Del(\"Referer\")\n\t\t\treturn nil\n\t\t}\n\t})\n\treturn httpClient\n}\n\nfunc NewHttpClient() *http.Client {\n\ttransport := &http.Transport{\n\t\tProxy:           http.ProxyFromEnvironment,\n\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},\n\t}\n\n\tSetProxyIfConfigured(transport)\n\n\treturn &http.Client{\n\t\tTimeout:   time.Hour * 48,\n\t\tTransport: transport,\n\t}\n}\n"
  },
  {
    "path": "internal/net/util.go",
    "content": "package net\n\nimport (\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/rclone/rclone/lib/readers\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// scanETag determines if a syntactically valid ETag is present at s. If so,\n// the ETag and remaining text after consuming ETag is returned. Otherwise,\n// it returns \"\", \"\".\nfunc scanETag(s string) (etag string, remain string) {\n\ts = textproto.TrimString(s)\n\tstart := 0\n\tif strings.HasPrefix(s, \"W/\") {\n\t\tstart = 2\n\t}\n\tif len(s[start:]) < 2 || s[start] != '\"' {\n\t\treturn \"\", \"\"\n\t}\n\t// ETag is either W/\"text\" or \"text\".\n\t// See RFC 7232 2.3.\n\tfor i := start + 1; i < len(s); i++ {\n\t\tc := s[i]\n\t\tswitch {\n\t\t// Character values allowed in ETags.\n\t\tcase c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:\n\t\tcase c == '\"':\n\t\t\treturn s[:i+1], s[i+1:]\n\t\tdefault:\n\t\t\treturn \"\", \"\"\n\t\t}\n\t}\n\treturn \"\", \"\"\n}\n\n// etagStrongMatch reports whether a and b match using strong ETag comparison.\n// Assumes a and b are valid ETags.\nfunc etagStrongMatch(a, b string) bool {\n\treturn a == b && a != \"\" && a[0] == '\"'\n}\n\n// etagWeakMatch reports whether a and b match using weak ETag comparison.\n// Assumes a and b are valid ETags.\nfunc etagWeakMatch(a, b string) bool {\n\treturn strings.TrimPrefix(a, \"W/\") == strings.TrimPrefix(b, \"W/\")\n}\n\n// condResult is the result of an HTTP request precondition check.\n// See https://tools.ietf.org/html/rfc7232 section 3.\ntype condResult int\n\nconst (\n\tcondNone condResult = iota\n\tcondTrue\n\tcondFalse\n)\n\nfunc checkIfMatch(w http.ResponseWriter, r *http.Request) condResult {\n\tim := r.Header.Get(\"If-Match\")\n\tif im == \"\" {\n\t\treturn condNone\n\t}\n\tr.Header.Del(\"If-Match\")\n\tfor {\n\t\tim = textproto.TrimString(im)\n\t\tif len(im) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif im[0] == ',' {\n\t\t\tim = im[1:]\n\t\t\tcontinue\n\t\t}\n\t\tif im[0] == '*' {\n\t\t\treturn condTrue\n\t\t}\n\t\tetag, remain := scanETag(im)\n\t\tif etag == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tif etagStrongMatch(etag, w.Header().Get(\"Etag\")) {\n\t\t\treturn condTrue\n\t\t}\n\t\tim = remain\n\t}\n\n\treturn condFalse\n}\n\nfunc checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {\n\tius := r.Header.Get(\"If-Unmodified-Since\")\n\tif ius == \"\" {\n\t\treturn condNone\n\t}\n\tr.Header.Del(\"If-Unmodified-Since\")\n\tif isZeroTime(modtime) {\n\t\treturn condNone\n\t}\n\tt, err := http.ParseTime(ius)\n\tif err != nil {\n\t\treturn condNone\n\t}\n\n\t// The Last-Modified header truncates sub-second precision so\n\t// the modtime needs to be truncated too.\n\tmodtime = modtime.Truncate(time.Second)\n\tif ret := modtime.Compare(t); ret <= 0 {\n\t\treturn condTrue\n\t}\n\treturn condFalse\n}\n\nfunc checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult {\n\tinm := r.Header.Get(\"If-None-Match\")\n\tif inm == \"\" {\n\t\treturn condNone\n\t}\n\tr.Header.Del(\"If-None-Match\")\n\tbuf := inm\n\tfor {\n\t\tbuf = textproto.TrimString(buf)\n\t\tif len(buf) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif buf[0] == ',' {\n\t\t\tbuf = buf[1:]\n\t\t\tcontinue\n\t\t}\n\t\tif buf[0] == '*' {\n\t\t\treturn condFalse\n\t\t}\n\t\tetag, remain := scanETag(buf)\n\t\tif etag == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tif etagWeakMatch(etag, w.Header().Get(\"Etag\")) {\n\t\t\treturn condFalse\n\t\t}\n\t\tbuf = remain\n\t}\n\treturn condTrue\n}\n\nfunc checkIfModifiedSince(r *http.Request, modtime time.Time) condResult {\n\tif r.Method != \"GET\" && r.Method != \"HEAD\" {\n\t\treturn condNone\n\t}\n\tims := r.Header.Get(\"If-Modified-Since\")\n\tif ims == \"\" {\n\t\treturn condNone\n\t}\n\tr.Header.Del(\"If-Modified-Since\")\n\tif isZeroTime(modtime) {\n\t\treturn condNone\n\t}\n\tt, err := http.ParseTime(ims)\n\tif err != nil {\n\t\treturn condNone\n\t}\n\t// The Last-Modified header truncates sub-second precision so\n\t// the modtime needs to be truncated too.\n\tmodtime = modtime.Truncate(time.Second)\n\tif ret := modtime.Compare(t); ret <= 0 {\n\t\treturn condFalse\n\t}\n\treturn condTrue\n}\n\nfunc checkIfRange(w http.ResponseWriter, r *http.Request, modtime time.Time) condResult {\n\tif r.Method != \"GET\" && r.Method != \"HEAD\" {\n\t\treturn condNone\n\t}\n\tir := r.Header.Get(\"If-Range\")\n\tif ir == \"\" {\n\t\treturn condNone\n\t}\n\tr.Header.Del(\"If-Range\")\n\tetag, _ := scanETag(ir)\n\tif etag != \"\" {\n\t\tif etagStrongMatch(etag, w.Header().Get(\"Etag\")) {\n\t\t\treturn condTrue\n\t\t}\n\t\treturn condFalse\n\t}\n\t// The If-Range value is typically the ETag value, but it may also be\n\t// the modtime date. See golang.org/issue/8367.\n\tif modtime.IsZero() {\n\t\treturn condFalse\n\t}\n\tt, err := http.ParseTime(ir)\n\tif err != nil {\n\t\treturn condFalse\n\t}\n\tif t.Unix() == modtime.Unix() {\n\t\treturn condTrue\n\t}\n\treturn condFalse\n}\n\nvar unixEpochTime = time.Unix(0, 0)\n\n// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).\nfunc isZeroTime(t time.Time) bool {\n\treturn t.IsZero() || t.Equal(unixEpochTime)\n}\n\nfunc setLastModified(w http.ResponseWriter, modtime time.Time) {\n\tif !isZeroTime(modtime) {\n\t\tw.Header().Set(\"Last-Modified\", modtime.UTC().Format(http.TimeFormat))\n\t}\n}\n\nfunc writeNotModified(w http.ResponseWriter) {\n\t// RFC 7232 section 4.1:\n\t// a sender SHOULD NOT generate representation metadata other than the\n\t// above listed fields unless said metadata exists for the purpose of\n\t// guiding cache updates (e.g., Last-Modified might be useful if the\n\t// response does not have an ETag field).\n\th := w.Header()\n\tdelete(h, \"Content-Type\")\n\tdelete(h, \"Content-Length\")\n\tdelete(h, \"Content-Encoding\")\n\tif h.Get(\"Etag\") != \"\" {\n\t\tdelete(h, \"Last-Modified\")\n\t}\n\tw.WriteHeader(http.StatusNotModified)\n}\n\n// checkPreconditions evaluates request preconditions and reports whether a precondition\n// resulted in sending StatusNotModified or StatusPreconditionFailed.\nfunc checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool, rangeHeader string) {\n\t// This function carefully follows RFC 7232 section 6.\n\tch := checkIfMatch(w, r)\n\tif ch == condNone {\n\t\tch = checkIfUnmodifiedSince(r, modtime)\n\t}\n\tif ch == condFalse {\n\t\tw.WriteHeader(http.StatusPreconditionFailed)\n\t\treturn true, \"\"\n\t}\n\tswitch checkIfNoneMatch(w, r) {\n\tcase condFalse:\n\t\tif r.Method == \"GET\" || r.Method == \"HEAD\" {\n\t\t\twriteNotModified(w)\n\t\t\treturn true, \"\"\n\t\t}\n\t\tw.WriteHeader(http.StatusPreconditionFailed)\n\t\treturn true, \"\"\n\tcase condNone:\n\t\tif checkIfModifiedSince(r, modtime) == condFalse {\n\t\t\twriteNotModified(w)\n\t\t\treturn true, \"\"\n\t\t}\n\t}\n\n\trangeHeader = r.Header.Get(\"Range\")\n\tif rangeHeader != \"\" && checkIfRange(w, r, modtime) == condFalse {\n\t\trangeHeader = \"\"\n\t}\n\treturn false, rangeHeader\n}\n\nfunc sumRangesSize(ranges []http_range.Range) (size int64) {\n\tfor _, ra := range ranges {\n\t\tsize += ra.Length\n\t}\n\treturn\n}\n\n// countingWriter counts how many bytes have been written to it.\ntype countingWriter int64\n\nfunc (w *countingWriter) Write(p []byte) (n int, err error) {\n\t*w += countingWriter(len(p))\n\treturn len(p), nil\n}\n\n// rangesMIMESize returns the number of bytes it takes to encode the\n// provided ranges as a multipart response.\nfunc rangesMIMESize(ranges []http_range.Range, contentType string, contentSize int64) (encSize int64, err error) {\n\tvar w countingWriter\n\tmw := multipart.NewWriter(&w)\n\tfor _, ra := range ranges {\n\t\t_, err := mw.CreatePart(ra.MimeHeader(contentType, contentSize))\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tencSize += ra.Length\n\t}\n\terr = mw.Close()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tencSize += int64(w)\n\treturn encSize, nil\n}\n\n// GetRangedHttpReader some http server doesn't support \"Range\" header,\n// so this function read readCloser with whole data, skip offset, then return ReaderCloser.\nfunc GetRangedHttpReader(readCloser io.ReadCloser, offset, length int64) (io.ReadCloser, error) {\n\n\tif offset > 100*1024*1024 {\n\t\tlog.Warnf(\"offset is more than 100MB, if loading data from internet, high-latency and wasting of bandwidth is expected\")\n\t}\n\n\tif _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(readCloser, offset)); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// return an io.ReadCloser that is limited to `length` bytes.\n\treturn readers.NewLimitedReadCloser(readCloser, length), nil\n}\n\n// SetProxyIfConfigured sets proxy for HTTP Transport if configured\nfunc SetProxyIfConfigured(transport *http.Transport) {\n\t// If proxy address is configured, override environment variable settings\n\tif conf.Conf.ProxyAddress != \"\" {\n\t\tif proxyURL, err := url.Parse(conf.Conf.ProxyAddress); err == nil {\n\t\t\ttransport.Proxy = http.ProxyURL(proxyURL)\n\t\t}\n\t}\n}\n\n// SetRestyProxyIfConfigured sets proxy for Resty client if configured\nfunc SetRestyProxyIfConfigured(client *resty.Client) {\n\t// If proxy address is configured, override environment variable settings\n\tif conf.Conf.ProxyAddress != \"\" {\n\t\tif proxyURL, err := url.Parse(conf.Conf.ProxyAddress); err == nil {\n\t\t\tclient.SetProxy(proxyURL.String())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/offline_download/115/client.go",
    "content": "package _115\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t_115 \"github.com/OpenListTeam/OpenList/v4/drivers/115\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Cloud115 struct {\n\trefreshTaskCache bool\n}\n\nfunc (p *Cloud115) Name() string {\n\treturn \"115 Cloud\"\n}\n\nfunc (p *Cloud115) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (p *Cloud115) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (p *Cloud115) Init() (string, error) {\n\tp.refreshTaskCache = false\n\treturn \"ok\", nil\n}\n\nfunc (p *Cloud115) IsReady() bool {\n\ttempDir := setting.GetStr(conf.Pan115TempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*_115.Pan115); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (p *Cloud115) AddURL(args *tool.AddUrlArgs) (string, error) {\n\t// 添加新任务刷新缓存\n\tp.refreshTaskCache = true\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdriver115, ok := storage.(*_115.Pan115)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only 115 Cloud is supported\")\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thashs, err := driver115.OfflineDownload(ctx, []string{args.Url}, parentDir)\n\tif err != nil || len(hashs) < 1 {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\treturn hashs[0], nil\n}\n\nfunc (p *Cloud115) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdriver115, ok := storage.(*_115.Pan115)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only 115 Cloud is supported\")\n\t}\n\n\tctx := context.Background()\n\tif err := driver115.DeleteOfflineTasks(ctx, []string{task.GID}, false); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (p *Cloud115) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdriver115, ok := storage.(*_115.Pan115)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only 115 Cloud is supported\")\n\t}\n\n\ttasks, err := driver115.OfflineList(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\tfor _, t := range tasks {\n\t\tif t.InfoHash == task.GID {\n\t\t\ts.Progress = t.Percent\n\t\t\ts.Status = t.GetStatus()\n\t\t\ts.Completed = t.IsDone()\n\t\t\ts.TotalBytes = t.Size\n\t\t\tif t.IsFailed() {\n\t\t\t\ts.Err = fmt.Errorf(t.GetStatus())\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn nil, nil\n}\n\nvar _ tool.Tool = (*Cloud115)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Cloud115{})\n}\n"
  },
  {
    "path": "internal/offline_download/115_open/client.go",
    "content": "package _115_open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t_115_open \"github.com/OpenListTeam/OpenList/v4/drivers/115_open\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Open115 struct {\n}\n\nfunc (o *Open115) Name() string {\n\treturn \"115 Open\"\n}\n\nfunc (o *Open115) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (o *Open115) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (o *Open115) Init() (string, error) {\n\treturn \"ok\", nil\n}\n\nfunc (o *Open115) IsReady() bool {\n\ttempDir := setting.GetStr(conf.Pan115OpenTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*_115_open.Open115); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (o *Open115) AddURL(args *tool.AddUrlArgs) (string, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdriver115Open, ok := storage.(*_115_open.Open115)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only 115 Cloud is supported\")\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thashs, err := driver115Open.OfflineDownload(ctx, []string{args.Url}, parentDir)\n\tif err != nil || len(hashs) < 1 {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\treturn hashs[0], nil\n}\n\nfunc (o *Open115) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdriver115Open, ok := storage.(*_115_open.Open115)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only 115 Open is supported\")\n\t}\n\n\tctx := context.Background()\n\tif err := driver115Open.DeleteOfflineTask(ctx, task.GID, false); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (o *Open115) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdriver115Open, ok := storage.(*_115_open.Open115)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only 115 Open is supported\")\n\t}\n\n\ttasks, err := driver115Open.OfflineList(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\n\tfor _, t := range tasks.Tasks {\n\t\tif t.InfoHash == task.GID {\n\t\t\ts.Progress = float64(t.PercentDone)\n\t\t\ts.Status = t.GetStatus()\n\t\t\ts.Completed = t.IsDone()\n\t\t\ts.TotalBytes = t.Size\n\t\t\tif t.IsFailed() {\n\t\t\t\ts.Err = fmt.Errorf(t.GetStatus())\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn nil, nil\n}\n\nvar _ tool.Tool = (*Open115)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Open115{})\n}\n"
  },
  {
    "path": "internal/offline_download/123/client.go",
    "content": "package _123_pan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t_123 \"github.com/OpenListTeam/OpenList/v4/drivers/123\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n)\n\ntype Pan123 struct{}\n\nfunc (*Pan123) Name() string {\n\treturn \"123Pan\"\n}\n\nfunc (*Pan123) Items() []model.SettingItem {\n\treturn []model.SettingItem{\n\t\t{Key: conf.Pan123TempDir, Value: \"\", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n}\n\nfunc (*Pan123) Run(_ *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (*Pan123) Init() (string, error) {\n\treturn \"ok\", nil\n}\n\nfunc (*Pan123) IsReady() bool {\n\ttempDir := setting.GetStr(conf.Pan123TempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*_123.Pan123); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (*Pan123) AddURL(args *tool.AddUrlArgs) (string, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdriver123, ok := storage.(*_123.Pan123)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only 123Pan is supported\")\n\t}\n\tctx := context.Background()\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttaskID, err := driver123.OfflineDownload(ctx, args.Url, parentDir)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\treturn strconv.FormatInt(taskID, 10), nil\n}\n\nfunc (*Pan123) Remove(task *tool.DownloadTask) error {\n\ttaskID, err := strconv.ParseInt(task.GID, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse task ID: %s\", task.GID)\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdriver123, ok := storage.(*_123.Pan123)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only 123Pan is supported\")\n\t}\n\treturn driver123.DeleteOfflineTasks(context.Background(), []int64{taskID})\n}\n\nfunc (*Pan123) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\ttaskID, err := strconv.ParseInt(task.GID, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse task ID: %s\", task.GID)\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdriver123, ok := storage.(*_123.Pan123)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only 123Pan is supported\")\n\t}\n\n\tt, err := driver123.GetOfflineTask(context.Background(), taskID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar statusStr string\n\tcompleted := false\n\tvar taskErr error\n\tswitch t.Status {\n\tcase 0:\n\t\tstatusStr = \"downloading\"\n\tcase 2:\n\t\tstatusStr = \"succeed\"\n\t\tcompleted = true\n\tcase 1:\n\t\tstatusStr = \"failed\"\n\t\ttaskErr = fmt.Errorf(\"offline download failed\")\n\tcase 3:\n\t\tstatusStr = \"retrying\"\n\tdefault:\n\t\tstatusStr = fmt.Sprintf(\"status_%d\", t.Status)\n\t}\n\n\treturn &tool.Status{\n\t\tTotalBytes: t.Size,\n\t\tProgress:   t.Progress,\n\t\tCompleted:  completed,\n\t\tStatus:     statusStr,\n\t\tErr:        taskErr,\n\t}, nil\n}\n\nvar _ tool.Tool = (*Pan123)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Pan123{})\n}\n"
  },
  {
    "path": "internal/offline_download/123_open/client.go",
    "content": "package _123_open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t_123_open \"github.com/OpenListTeam/OpenList/v4/drivers/123_open\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n)\n\ntype Open123 struct{}\n\nfunc (*Open123) Name() string {\n\treturn \"123 Open\"\n}\n\nfunc (*Open123) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (*Open123) Run(_ *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (*Open123) Init() (string, error) {\n\treturn \"ok\", nil\n}\n\nfunc (*Open123) IsReady() bool {\n\ttempDir := setting.GetStr(conf.Pan123OpenTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*_123_open.Open123); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (*Open123) AddURL(args *tool.AddUrlArgs) (string, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdriver123Open, ok := storage.(*_123_open.Open123)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only 123 Open is supported\")\n\t}\n\tctx := context.Background()\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcb := setting.GetStr(conf.Pan123OpenOfflineDownloadCallbackUrl)\n\ttaskID, err := driver123Open.OfflineDownload(ctx, args.Url, parentDir, cb)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\treturn strconv.Itoa(taskID), nil\n}\n\nfunc (*Open123) Remove(_ *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (*Open123) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\ttaskID, err := strconv.Atoi(task.GID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse task ID: %s\", task.GID)\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdriver123Open, ok := storage.(*_123_open.Open123)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only 123 Open is supported\")\n\t}\n\tprocess, status, err := driver123Open.OfflineDownloadProcess(context.Background(), taskID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar statusStr string\n\tswitch status {\n\tcase 0:\n\t\tstatusStr = \"downloading\"\n\tcase 1:\n\t\terr = fmt.Errorf(\"offline download failed\")\n\tcase 2:\n\t\tstatusStr = \"succeed\"\n\tcase 3:\n\t\tstatusStr = \"retrying\"\n\t}\n\treturn &tool.Status{\n\t\tProgress:  process,\n\t\tCompleted: status == 2,\n\t\tStatus:    statusStr,\n\t\tErr:       err,\n\t}, nil\n}\n\nvar _ tool.Tool = (*Open123)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Open123{})\n}\n"
  },
  {
    "path": "internal/offline_download/all.go",
    "content": "package offline_download\n\nimport (\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/115\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/115_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/123\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/123_open\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/aria2\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/http\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/pikpak\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/qbit\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/thunder\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/thunder_browser\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/thunderx\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/offline_download/transmission\"\n)\n"
  },
  {
    "path": "internal/offline_download/aria2/aria2.go",
    "content": "package aria2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/aria2/rpc\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar notify = NewNotify()\n\ntype Aria2 struct {\n\tclient rpc.Client\n}\n\nfunc (a *Aria2) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (a *Aria2) Name() string {\n\treturn \"aria2\"\n}\n\nfunc (a *Aria2) Items() []model.SettingItem {\n\t// aria2 settings\n\treturn []model.SettingItem{\n\t\t{Key: conf.Aria2Uri, Value: \"http://localhost:6800/jsonrpc\", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.Aria2Secret, Value: \"\", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n}\n\nfunc (a *Aria2) Init() (string, error) {\n\ta.client = nil\n\turi := setting.GetStr(conf.Aria2Uri)\n\tsecret := setting.GetStr(conf.Aria2Secret)\n\tc, err := rpc.New(context.Background(), uri, secret, 4*time.Second, notify)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to init aria2 client\")\n\t}\n\tversion, err := c.GetVersion()\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed get aria2 version\")\n\t}\n\ta.client = c\n\tlog.Infof(\"using aria2 version: %s\", version.Version)\n\treturn fmt.Sprintf(\"aria2 version: %s\", version.Version), nil\n}\n\nfunc (a *Aria2) IsReady() bool {\n\treturn a.client != nil\n}\n\nfunc (a *Aria2) AddURL(args *tool.AddUrlArgs) (string, error) {\n\toptions := map[string]interface{}{\n\t\t\"dir\": args.TempDir,\n\t}\n\tgid, err := a.client.AddURI([]string{args.Url}, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tnotify.Signals.Store(gid, args.Signal)\n\treturn gid, nil\n}\n\nfunc (a *Aria2) Remove(task *tool.DownloadTask) error {\n\t_, err := a.client.Remove(task.GID)\n\treturn err\n}\n\nfunc (a *Aria2) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tinfo, err := a.client.TellStatus(task.GID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := strconv.ParseInt(info.TotalLength, 10, 64)\n\tif err != nil {\n\t\ttotal = 0\n\t}\n\tdownloaded, err := strconv.ParseUint(info.CompletedLength, 10, 64)\n\tif err != nil {\n\t\tdownloaded = 0\n\t}\n\ts := &tool.Status{\n\t\tCompleted:  info.Status == \"complete\",\n\t\tErr:        err,\n\t\tTotalBytes: total,\n\t}\n\ts.Progress = float64(downloaded) / float64(total) * 100\n\tif len(info.FollowedBy) != 0 {\n\t\ts.NewGID = info.FollowedBy[0]\n\t\tnotify.Signals.Delete(task.GID)\n\t\tnotify.Signals.Store(s.NewGID, task.Signal)\n\t}\n\tswitch info.Status {\n\tcase \"complete\":\n\t\ts.Completed = true\n\tcase \"error\":\n\t\ts.Err = errors.Errorf(\"failed to download %s, error: %s\", task.GID, info.ErrorMessage)\n\tcase \"active\":\n\t\ts.Status = \"aria2: \" + info.Status\n\t\tif info.Seeder == \"true\" {\n\t\t\ts.Completed = true\n\t\t}\n\tcase \"waiting\", \"paused\":\n\t\ts.Status = \"aria2: \" + info.Status\n\tcase \"removed\":\n\t\ts.Err = errors.Errorf(\"failed to download %s, removed\", task.GID)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"[aria2] unknown status %s\", info.Status)\n\t}\n\treturn s, nil\n}\n\nvar _ tool.Tool = (*Aria2)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Aria2{})\n}\n"
  },
  {
    "path": "internal/offline_download/aria2/notify.go",
    "content": "package aria2\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/aria2/rpc\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync\"\n)\n\nconst (\n\tDownloading = iota\n\tPaused\n\tStopped\n\tCompleted\n\tErrored\n)\n\ntype Notify struct {\n\tSignals generic_sync.MapOf[string, chan int]\n}\n\nfunc NewNotify() *Notify {\n\treturn &Notify{Signals: generic_sync.MapOf[string, chan int]{}}\n}\n\nfunc (n *Notify) OnDownloadStart(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Downloading\n\t\t}\n\t}\n}\n\nfunc (n *Notify) OnDownloadPause(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Paused\n\t\t}\n\t}\n}\n\nfunc (n *Notify) OnDownloadStop(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Stopped\n\t\t}\n\t}\n}\n\nfunc (n *Notify) OnDownloadComplete(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Completed\n\t\t}\n\t}\n}\n\nfunc (n *Notify) OnDownloadError(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Errored\n\t\t}\n\t}\n}\n\nfunc (n *Notify) OnBtDownloadComplete(events []rpc.Event) {\n\tfor _, e := range events {\n\t\tif signal, ok := n.Signals.Load(e.Gid); ok {\n\t\t\tsignal <- Completed\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/offline_download/http/client.go",
    "content": "package http\n\nimport (\n\t\"fmt\"\n\t\"math/rand/v2\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype SimpleHttp struct {\n\tclient http.Client\n}\n\nfunc (s SimpleHttp) Name() string {\n\treturn \"SimpleHttp\"\n}\n\nfunc (s SimpleHttp) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (s SimpleHttp) Init() (string, error) {\n\treturn \"ok\", nil\n}\n\nfunc (s SimpleHttp) IsReady() bool {\n\treturn true\n}\n\nfunc (s SimpleHttp) AddURL(args *tool.AddUrlArgs) (string, error) {\n\tpanic(\"should not be called\")\n}\n\nfunc (s SimpleHttp) Remove(task *tool.DownloadTask) error {\n\tpanic(\"should not be called\")\n}\n\nfunc (s SimpleHttp) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tpanic(\"should not be called\")\n}\n\nfunc (s SimpleHttp) Run(task *tool.DownloadTask) error {\n\tstreamPut := task.DeletePolicy == tool.UploadDownloadStream\n\tmethod := http.MethodGet\n\tif streamPut {\n\t\tmethod = http.MethodHead\n\t}\n\treq, err := http.NewRequestWithContext(task.Ctx(), method, task.Url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"User-Agent\", base.UserAgent)\n\tif streamPut {\n\t\treq.Header.Set(\"Range\", \"bytes=0-\")\n\t}\n\tresp, err := s.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn fmt.Errorf(\"http status code %d\", resp.StatusCode)\n\t}\n\tfilename, err := parseFilenameFromContentDisposition(resp.Header.Get(\"Content-Disposition\"))\n\tif err != nil {\n\t\tfilename = path.Base(resp.Request.URL.Path)\n\t}\n\tfilename = strings.Trim(filename, \"/\")\n\tif len(filename) == 0 {\n\t\tfilename = fmt.Sprintf(\"%s-%d-%x\", strings.ReplaceAll(req.URL.Host, \".\", \"_\"), time.Now().UnixMilli(), rand.Uint32())\n\t}\n\tfileSize := resp.ContentLength\n\tif streamPut {\n\t\tif fileSize == 0 {\n\t\t\tstart, end, _ := http_range.ParseContentRange(resp.Header.Get(\"Content-Range\"))\n\t\t\tfileSize = start + end\n\t\t}\n\t\ttask.SetTotalBytes(fileSize)\n\t\ttask.TempDir = filename\n\t\treturn nil\n\t}\n\ttask.SetTotalBytes(fileSize)\n\t// save to temp dir\n\t_ = os.MkdirAll(task.TempDir, os.ModePerm)\n\tfilePath := filepath.Join(task.TempDir, filename)\n\tfile, err := os.Create(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\terr = utils.CopyWithCtx(task.Ctx(), file, resp.Body, fileSize, task.SetProgress)\n\treturn err\n}\n\nfunc init() {\n\ttool.Tools.Add(&SimpleHttp{})\n}\n"
  },
  {
    "path": "internal/offline_download/http/util.go",
    "content": "package http\n\nimport (\n\t\"fmt\"\n\t\"mime\"\n)\n\nfunc parseFilenameFromContentDisposition(contentDisposition string) (string, error) {\n\tif contentDisposition == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Content-Disposition is empty\")\n\t}\n\t_, params, err := mime.ParseMediaType(contentDisposition)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfilename := params[\"filename\"]\n\tif filename == \"\" {\n\t\treturn \"\", fmt.Errorf(\"filename not found in Content-Disposition: [%s]\", contentDisposition)\n\t}\n\treturn filename, nil\n}\n"
  },
  {
    "path": "internal/offline_download/pikpak/pikpak.go",
    "content": "package pikpak\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/pikpak\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype PikPak struct {\n\trefreshTaskCache bool\n}\n\nfunc (p *PikPak) Name() string {\n\treturn \"PikPak\"\n}\n\nfunc (p *PikPak) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (p *PikPak) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (p *PikPak) Init() (string, error) {\n\tp.refreshTaskCache = false\n\treturn \"ok\", nil\n}\n\nfunc (p *PikPak) IsReady() bool {\n\ttempDir := setting.GetStr(conf.PikPakTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*pikpak.PikPak); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (p *PikPak) AddURL(args *tool.AddUrlArgs) (string, error) {\n\t// 添加新任务刷新缓存\n\tp.refreshTaskCache = true\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpikpakDriver, ok := storage.(*pikpak.PikPak)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only Pikpak is supported\")\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tt, err := pikpakDriver.OfflineDownload(ctx, args.Url, parentDir, \"\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\treturn t.ID, nil\n}\n\nfunc (p *PikPak) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpikpakDriver, ok := storage.(*pikpak.PikPak)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only Pikpak is supported\")\n\t}\n\tctx := context.Background()\n\terr = pikpakDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (p *PikPak) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpikpakDriver, ok := storage.(*pikpak.PikPak)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only Pikpak is supported\")\n\t}\n\ttasks, err := p.GetTasks(pikpakDriver)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\tfor _, t := range tasks {\n\t\tif t.ID == task.GID {\n\t\t\ts.Progress = float64(t.Progress)\n\t\t\ts.Status = t.Message\n\t\t\ts.Completed = (t.Phase == \"PHASE_TYPE_COMPLETE\")\n\t\t\ts.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\ts.TotalBytes = 0\n\t\t\t}\n\t\t\tif t.Phase == \"PHASE_TYPE_ERROR\" {\n\t\t\t\ts.Err = fmt.Errorf(t.Message)\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn s, nil\n}\n\nfunc init() {\n\ttool.Tools.Add(&PikPak{})\n}\n"
  },
  {
    "path": "internal/offline_download/pikpak/util.go",
    "content": "package pikpak\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/pikpak\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/go-cache\"\n)\n\nvar taskCache = cache.NewMemCache(cache.WithShards[[]pikpak.OfflineTask](16))\nvar taskG singleflight.Group[[]pikpak.OfflineTask]\n\nfunc (p *PikPak) GetTasks(pikpakDriver *pikpak.PikPak) ([]pikpak.OfflineTask, error) {\n\tkey := op.Key(pikpakDriver, \"/drive/v1/task\")\n\tif !p.refreshTaskCache {\n\t\tif tasks, ok := taskCache.Get(key); ok {\n\t\t\treturn tasks, nil\n\t\t}\n\t}\n\tp.refreshTaskCache = false\n\ttasks, err, _ := taskG.Do(key, func() ([]pikpak.OfflineTask, error) {\n\t\tctx := context.Background()\n\t\tphase := []string{\"PHASE_TYPE_RUNNING\", \"PHASE_TYPE_ERROR\", \"PHASE_TYPE_PENDING\", \"PHASE_TYPE_COMPLETE\"}\n\t\ttasks, err := pikpakDriver.OfflineList(ctx, \"\", phase)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 添加缓存 10s\n\t\tif len(tasks) > 0 {\n\t\t\ttaskCache.Set(key, tasks, cache.WithEx[[]pikpak.OfflineTask](time.Second*10))\n\t\t} else {\n\t\t\ttaskCache.Del(key)\n\t\t}\n\t\treturn tasks, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tasks, nil\n}\n"
  },
  {
    "path": "internal/offline_download/qbit/qbit.go",
    "content": "package qbit\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/qbittorrent\"\n\t\"github.com/pkg/errors\"\n)\n\ntype QBittorrent struct {\n\tclient qbittorrent.Client\n}\n\nfunc (a *QBittorrent) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (a *QBittorrent) Name() string {\n\treturn \"qBittorrent\"\n}\n\nfunc (a *QBittorrent) Items() []model.SettingItem {\n\t// qBittorrent settings\n\treturn []model.SettingItem{\n\t\t{Key: conf.QbittorrentUrl, Value: \"http://admin:adminadmin@localhost:8080/\", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.QbittorrentSeedtime, Value: \"0\", Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n}\n\nfunc (a *QBittorrent) Init() (string, error) {\n\ta.client = nil\n\turl := setting.GetStr(conf.QbittorrentUrl)\n\tqbClient, err := qbittorrent.New(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ta.client = qbClient\n\treturn \"ok\", nil\n}\n\nfunc (a *QBittorrent) IsReady() bool {\n\treturn a.client != nil\n}\n\nfunc (a *QBittorrent) AddURL(args *tool.AddUrlArgs) (string, error) {\n\terr := a.client.AddFromLink(args.Url, args.TempDir, args.UID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn args.UID, nil\n}\n\nfunc (a *QBittorrent) Remove(task *tool.DownloadTask) error {\n\terr := a.client.Delete(task.GID, false)\n\treturn err\n}\n\nfunc (a *QBittorrent) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tinfo, err := a.client.GetInfo(task.GID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts := &tool.Status{}\n\ts.TotalBytes = info.Size\n\ts.Progress = float64(info.Completed) / float64(info.Size) * 100\n\tswitch info.State {\n\tcase qbittorrent.UPLOADING, qbittorrent.PAUSEDUP, qbittorrent.QUEUEDUP, qbittorrent.STALLEDUP, qbittorrent.FORCEDUP, qbittorrent.CHECKINGUP:\n\t\ts.Completed = true\n\tcase qbittorrent.ALLOCATING, qbittorrent.DOWNLOADING, qbittorrent.METADL, qbittorrent.PAUSEDDL, qbittorrent.QUEUEDDL, qbittorrent.STALLEDDL, qbittorrent.CHECKINGDL, qbittorrent.FORCEDDL, qbittorrent.CHECKINGRESUMEDATA, qbittorrent.MOVING:\n\t\ts.Status = \"[qBittorrent] downloading\"\n\tcase qbittorrent.ERROR, qbittorrent.MISSINGFILES, qbittorrent.UNKNOWN:\n\t\ts.Err = errors.Errorf(\"[qBittorrent] failed to download %s, error: %s\", task.GID, info.State)\n\tdefault:\n\t\ts.Err = errors.Errorf(\"[qBittorrent] unknown error occurred downloading %s\", task.GID)\n\t}\n\treturn s, nil\n}\n\nvar _ tool.Tool = (*QBittorrent)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&QBittorrent{})\n}\n"
  },
  {
    "path": "internal/offline_download/thunder/thunder.go",
    "content": "package thunder\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype Thunder struct {\n\trefreshTaskCache bool\n}\n\nfunc (t *Thunder) Name() string {\n\treturn \"Thunder\"\n}\n\nfunc (t *Thunder) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (t *Thunder) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (t *Thunder) Init() (string, error) {\n\tt.refreshTaskCache = false\n\treturn \"ok\", nil\n}\n\nfunc (t *Thunder) IsReady() bool {\n\ttempDir := setting.GetStr(conf.ThunderTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*thunder.Thunder); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (t *Thunder) AddURL(args *tool.AddUrlArgs) (string, error) {\n\t// 添加新任务刷新缓存\n\tt.refreshTaskCache = true\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tthunderDriver, ok := storage.(*thunder.Thunder)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only Thunder is supported\")\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttask, err := thunderDriver.OfflineDownload(ctx, args.Url, parentDir, \"\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\treturn task.ID, nil\n}\n\nfunc (t *Thunder) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tthunderDriver, ok := storage.(*thunder.Thunder)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only Thunder is supported\")\n\t}\n\tctx := context.Background()\n\terr = thunderDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (t *Thunder) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tthunderDriver, ok := storage.(*thunder.Thunder)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only Thunder is supported\")\n\t}\n\ttasks, err := t.GetTasks(thunderDriver)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\tfor _, t := range tasks {\n\t\tif t.ID == task.GID {\n\t\t\ts.Progress = float64(t.Progress)\n\t\t\ts.Status = t.Message\n\t\t\ts.Completed = (t.Phase == \"PHASE_TYPE_COMPLETE\")\n\t\t\ts.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\ts.TotalBytes = 0\n\t\t\t}\n\t\t\tif t.Phase == \"PHASE_TYPE_ERROR\" {\n\t\t\t\ts.Err = errors.New(t.Message)\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn s, nil\n}\n\nfunc init() {\n\ttool.Tools.Add(&Thunder{})\n}\n"
  },
  {
    "path": "internal/offline_download/thunder/util.go",
    "content": "package thunder\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/go-cache\"\n)\n\nvar taskCache = cache.NewMemCache(cache.WithShards[[]thunder.OfflineTask](16))\nvar taskG singleflight.Group[[]thunder.OfflineTask]\n\nfunc (t *Thunder) GetTasks(thunderDriver *thunder.Thunder) ([]thunder.OfflineTask, error) {\n\tkey := op.Key(thunderDriver, \"/drive/v1/task\")\n\tif !t.refreshTaskCache {\n\t\tif tasks, ok := taskCache.Get(key); ok {\n\t\t\treturn tasks, nil\n\t\t}\n\t}\n\tt.refreshTaskCache = false\n\ttasks, err, _ := taskG.Do(key, func() ([]thunder.OfflineTask, error) {\n\t\tctx := context.Background()\n\t\ttasks, err := thunderDriver.OfflineList(ctx, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 添加缓存 10s\n\t\tif len(tasks) > 0 {\n\t\t\ttaskCache.Set(key, tasks, cache.WithEx[[]thunder.OfflineTask](time.Second*10))\n\t\t} else {\n\t\t\ttaskCache.Del(key)\n\t\t}\n\t\treturn tasks, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tasks, nil\n}\n"
  },
  {
    "path": "internal/offline_download/thunder_browser/thunder_browser.go",
    "content": "package thunder_browser\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\ntype ThunderBrowser struct {\n\trefreshTaskCache bool\n}\n\nfunc (t *ThunderBrowser) Name() string {\n\treturn \"ThunderBrowser\"\n}\n\nfunc (t *ThunderBrowser) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (t *ThunderBrowser) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (t *ThunderBrowser) Init() (string, error) {\n\tt.refreshTaskCache = false\n\treturn \"ok\", nil\n}\n\nfunc (t *ThunderBrowser) IsReady() bool {\n\ttempDir := setting.GetStr(conf.ThunderBrowserTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tswitch storage.(type) {\n\tcase *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (t *ThunderBrowser) AddURL(args *tool.AddUrlArgs) (string, error) {\n\t// 添加新任务刷新缓存\n\tt.refreshTaskCache = true\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar task *thunder_browser.OfflineTask\n\tswitch v := storage.(type) {\n\tcase *thunder_browser.ThunderBrowser:\n\t\ttask, err = v.OfflineDownload(ctx, args.Url, parentDir, \"\")\n\tcase *thunder_browser.ThunderBrowserExpert:\n\t\ttask, err = v.OfflineDownload(ctx, args.Url, parentDir, \"\")\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only ThunderBrowser is supported\")\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\tif task == nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: task is nil\")\n\t}\n\n\treturn task.ID, nil\n}\n\nfunc (t *ThunderBrowser) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tswitch v := storage.(type) {\n\tcase *thunder_browser.ThunderBrowser:\n\t\terr = v.DeleteOfflineTasks(ctx, []string{task.GID})\n\tcase *thunder_browser.ThunderBrowserExpert:\n\t\terr = v.DeleteOfflineTasks(ctx, []string{task.GID})\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only ThunderBrowser is supported\")\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (t *ThunderBrowser) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tasks []thunder_browser.OfflineTask\n\n\tswitch v := storage.(type) {\n\tcase *thunder_browser.ThunderBrowser:\n\t\ttasks, err = t.GetTasks(v)\n\tcase *thunder_browser.ThunderBrowserExpert:\n\t\ttasks, err = t.GetTasksExpert(v)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only ThunderBrowser is supported\")\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\n\tfor _, t := range tasks {\n\t\tif t.ID == task.GID {\n\t\t\ts.Progress = float64(t.Progress)\n\t\t\ts.Status = t.Message\n\t\t\ts.Completed = t.Phase == \"PHASE_TYPE_COMPLETE\"\n\t\t\ts.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\ts.TotalBytes = 0\n\t\t\t}\n\t\t\tif t.Phase == \"PHASE_TYPE_ERROR\" {\n\t\t\t\ts.Err = errors.New(t.Message)\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn s, nil\n}\n\nfunc init() {\n\ttool.Tools.Add(&ThunderBrowser{})\n}\n"
  },
  {
    "path": "internal/offline_download/thunder_browser/util.go",
    "content": "package thunder_browser\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/go-cache\"\n)\n\nvar taskCache = cache.NewMemCache(cache.WithShards[[]thunder_browser.OfflineTask](16))\nvar taskG singleflight.Group[[]thunder_browser.OfflineTask]\n\nfunc (t *ThunderBrowser) GetTasks(thunderDriver *thunder_browser.ThunderBrowser) ([]thunder_browser.OfflineTask, error) {\n\tkey := op.Key(thunderDriver, \"/drive/v1/task\")\n\tif !t.refreshTaskCache {\n\t\tif tasks, ok := taskCache.Get(key); ok {\n\t\t\treturn tasks, nil\n\t\t}\n\t}\n\tt.refreshTaskCache = false\n\ttasks, err, _ := taskG.Do(key, func() ([]thunder_browser.OfflineTask, error) {\n\t\tctx := context.Background()\n\t\ttasks, err := thunderDriver.OfflineList(ctx, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 添加缓存 10s\n\t\tif len(tasks) > 0 {\n\t\t\ttaskCache.Set(key, tasks, cache.WithEx[[]thunder_browser.OfflineTask](time.Second*10))\n\t\t} else {\n\t\t\ttaskCache.Del(key)\n\t\t}\n\t\treturn tasks, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tasks, nil\n}\n\nfunc (t *ThunderBrowser) GetTasksExpert(thunderDriver *thunder_browser.ThunderBrowserExpert) ([]thunder_browser.OfflineTask, error) {\n\tkey := op.Key(thunderDriver, \"/drive/v1/task\")\n\tif !t.refreshTaskCache {\n\t\tif tasks, ok := taskCache.Get(key); ok {\n\t\t\treturn tasks, nil\n\t\t}\n\t}\n\tt.refreshTaskCache = false\n\ttasks, err, _ := taskG.Do(key, func() ([]thunder_browser.OfflineTask, error) {\n\t\tctx := context.Background()\n\t\ttasks, err := thunderDriver.OfflineList(ctx, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 添加缓存 10s\n\t\tif len(tasks) > 0 {\n\t\t\ttaskCache.Set(key, tasks, cache.WithEx[[]thunder_browser.OfflineTask](time.Second*10))\n\t\t} else {\n\t\t\ttaskCache.Del(key)\n\t\t}\n\t\treturn tasks, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tasks, nil\n}\n"
  },
  {
    "path": "internal/offline_download/thunderx/thunderx.go",
    "content": "package thunderx\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunderx\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"strconv\"\n)\n\ntype ThunderX struct {\n\trefreshTaskCache bool\n}\n\nfunc (t *ThunderX) Name() string {\n\treturn \"ThunderX\"\n}\n\nfunc (t *ThunderX) Items() []model.SettingItem {\n\treturn nil\n}\n\nfunc (t *ThunderX) Init() (string, error) {\n\tt.refreshTaskCache = false\n\treturn \"ok\", nil\n}\n\nfunc (t *ThunderX) IsReady() bool {\n\ttempDir := setting.GetStr(conf.ThunderXTempDir)\n\tif tempDir == \"\" {\n\t\treturn false\n\t}\n\tstorage, _, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, ok := storage.(*thunderx.ThunderX); !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (t *ThunderX) AddURL(args *tool.AddUrlArgs) (string, error) {\n\t// 添加新任务刷新缓存\n\tt.refreshTaskCache = true\n\tstorage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tthunderXDriver, ok := storage.(*thunderx.ThunderX)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"unsupported storage driver for offline download, only ThunderX is supported\")\n\t}\n\n\tctx := context.Background()\n\n\tif err := op.MakeDir(ctx, storage, actualPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentDir, err := op.GetUnwrap(ctx, storage, actualPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttask, err := thunderXDriver.OfflineDownload(ctx, args.Url, parentDir, \"\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to add offline download task: %w\", err)\n\t}\n\n\treturn task.ID, nil\n}\n\nfunc (t *ThunderX) Remove(task *tool.DownloadTask) error {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tthunderXDriver, ok := storage.(*thunderx.ThunderX)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported storage driver for offline download, only ThunderX is supported\")\n\t}\n\tctx := context.Background()\n\terr = thunderXDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (t *ThunderX) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tstorage, _, err := op.GetStorageAndActualPath(task.TempDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tthunderXDriver, ok := storage.(*thunderx.ThunderX)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported storage driver for offline download, only ThunderX is supported\")\n\t}\n\ttasks, err := t.GetTasks(thunderXDriver)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts := &tool.Status{\n\t\tProgress:  0,\n\t\tNewGID:    \"\",\n\t\tCompleted: false,\n\t\tStatus:    \"the task has been deleted\",\n\t\tErr:       nil,\n\t}\n\tfor _, t := range tasks {\n\t\tif t.ID == task.GID {\n\t\t\ts.Progress = float64(t.Progress)\n\t\t\ts.Status = t.Message\n\t\t\ts.Completed = t.Phase == \"PHASE_TYPE_COMPLETE\"\n\t\t\ts.TotalBytes, err = strconv.ParseInt(t.FileSize, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\ts.TotalBytes = 0\n\t\t\t}\n\t\t\tif t.Phase == \"PHASE_TYPE_ERROR\" {\n\t\t\t\ts.Err = errors.New(t.Message)\n\t\t\t}\n\t\t\treturn s, nil\n\t\t}\n\t}\n\ts.Err = fmt.Errorf(\"the task has been deleted\")\n\treturn s, nil\n}\n\nfunc (t *ThunderX) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc init() {\n\ttool.Tools.Add(&ThunderX{})\n}\n"
  },
  {
    "path": "internal/offline_download/thunderx/utils.go",
    "content": "package thunderx\n\nimport (\n\t\"context\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunderx\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"time\"\n)\n\nvar taskCache = cache.NewMemCache(cache.WithShards[[]thunderx.OfflineTask](16))\nvar taskG singleflight.Group[[]thunderx.OfflineTask]\n\nfunc (t *ThunderX) GetTasks(thunderxDriver *thunderx.ThunderX) ([]thunderx.OfflineTask, error) {\n\tkey := op.Key(thunderxDriver, \"/drive/v1/task\")\n\tif !t.refreshTaskCache {\n\t\tif tasks, ok := taskCache.Get(key); ok {\n\t\t\treturn tasks, nil\n\t\t}\n\t}\n\tt.refreshTaskCache = false\n\ttasks, err, _ := taskG.Do(key, func() ([]thunderx.OfflineTask, error) {\n\t\tctx := context.Background()\n\t\tphase := []string{\"PHASE_TYPE_RUNNING\", \"PHASE_TYPE_ERROR\", \"PHASE_TYPE_PENDING\", \"PHASE_TYPE_COMPLETE\"}\n\t\ttasks, err := thunderxDriver.OfflineList(ctx, \"\", phase)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// 添加缓存 10s\n\t\tif len(tasks) > 0 {\n\t\t\ttaskCache.Set(key, tasks, cache.WithEx[[]thunderx.OfflineTask](time.Second*10))\n\t\t} else {\n\t\t\ttaskCache.Del(key)\n\t\t}\n\t\treturn tasks, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tasks, nil\n}\n"
  },
  {
    "path": "internal/offline_download/tool/add.go",
    "content": "package tool\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\n\t_115 \"github.com/OpenListTeam/OpenList/v4/drivers/115\"\n\t_115_open \"github.com/OpenListTeam/OpenList/v4/drivers/115_open\"\n\t_123 \"github.com/OpenListTeam/OpenList/v4/drivers/123\"\n\t_123_open \"github.com/OpenListTeam/OpenList/v4/drivers/123_open\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/pikpak\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunderx\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n)\n\ntype DeletePolicy string\n\nconst (\n\tDeleteOnUploadSucceed DeletePolicy = \"delete_on_upload_succeed\"\n\tDeleteOnUploadFailed  DeletePolicy = \"delete_on_upload_failed\"\n\tDeleteNever           DeletePolicy = \"delete_never\"\n\tDeleteAlways          DeletePolicy = \"delete_always\"\n\tUploadDownloadStream  DeletePolicy = \"upload_download_stream\"\n)\n\ntype AddURLArgs struct {\n\tURL          string\n\tDstDirPath   string\n\tTool         string\n\tDeletePolicy DeletePolicy\n}\n\nfunc AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, error) {\n\t// check storage\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(args.DstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get storage\")\n\t}\n\t// check is it could upload\n\tif storage.Config().NoUpload {\n\t\treturn nil, errors.WithStack(errs.UploadNotSupported)\n\t}\n\t// check path is valid\n\tobj, err := op.Get(ctx, storage, dstDirActualPath)\n\tif err != nil {\n\t\tif !errs.IsObjectNotFound(err) {\n\t\t\treturn nil, errors.WithMessage(err, \"failed get object\")\n\t\t}\n\t} else {\n\t\tif !obj.IsDir() {\n\t\t\t// can't add to a file\n\t\t\treturn nil, errors.WithStack(errs.NotFolder)\n\t\t}\n\t}\n\t// try putting url\n\tif args.Tool == \"SimpleHttp\" {\n\t\terr = tryPutUrl(ctx, args.DstDirPath, args.URL)\n\t\tif err == nil || !errors.Is(err, errs.NotImplement) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// get tool\n\ttool, err := Tools.Get(args.Tool)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get offline download tool\")\n\t}\n\t// check tool is ready\n\tif !tool.IsReady() {\n\t\t// try to init tool\n\t\tif _, err := tool.Init(); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed init offline download tool %s\", args.Tool)\n\t\t}\n\t}\n\n\tuid := uuid.NewString()\n\ttempDir := filepath.Join(conf.Conf.TempDir, args.Tool, uid)\n\tdeletePolicy := args.DeletePolicy\n\n\t// 如果当前 storage 是对应网盘，则直接下载到目标路径，无需转存\n\tswitch args.Tool {\n\tcase \"115 Cloud\":\n\t\tif _, ok := storage.(*_115.Pan115); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.Pan115TempDir), uid)\n\t\t}\n\tcase \"115 Open\":\n\t\tif _, ok := storage.(*_115_open.Open115); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.Pan115OpenTempDir), uid)\n\t\t}\n\tcase \"123 Open\":\n\t\tif _, ok := storage.(*_123_open.Open123); ok && dstDirActualPath != \"/\" {\n\t\t\t// directly offline downloading to the root path is not allowed via 123 open platform\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.Pan123OpenTempDir), uid)\n\t\t}\n\tcase \"123Pan\":\n\t\tif _, ok := storage.(*_123.Pan123); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.Pan123TempDir), uid)\n\t\t}\n\tcase \"PikPak\":\n\t\tif _, ok := storage.(*pikpak.PikPak); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.PikPakTempDir), uid)\n\t\t}\n\tcase \"Thunder\":\n\t\tif _, ok := storage.(*thunder.Thunder); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.ThunderTempDir), uid)\n\t\t}\n\tcase \"ThunderBrowser\":\n\t\tswitch storage.(type) {\n\t\tcase *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:\n\t\t\ttempDir = args.DstDirPath\n\t\tdefault:\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.ThunderBrowserTempDir), uid)\n\t\t}\n\tcase \"ThunderX\":\n\t\tif _, ok := storage.(*thunderx.ThunderX); ok {\n\t\t\ttempDir = args.DstDirPath\n\t\t} else {\n\t\t\ttempDir = filepath.Join(setting.GetStr(conf.ThunderXTempDir), uid)\n\t\t}\n\t}\n\n\ttaskCreator, _ := ctx.Value(conf.UserKey).(*model.User) // taskCreator is nil when convert failed\n\tt := &DownloadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\n\t\t\tApiUrl:  common.GetApiUrl(ctx),\n\t\t},\n\t\tUrl:          args.URL,\n\t\tDstDirPath:   args.DstDirPath,\n\t\tTempDir:      tempDir,\n\t\tDeletePolicy: deletePolicy,\n\t\tToolname:     args.Tool,\n\t\ttool:         tool,\n\t}\n\tDownloadTaskManager.Add(t)\n\treturn t, nil\n}\n\nfunc tryPutUrl(ctx context.Context, path, urlStr string) error {\n\tvar dstName string\n\tu, err := url.Parse(urlStr)\n\tif err == nil {\n\t\tdstName = stdpath.Base(u.Path)\n\t} else {\n\t\tdstName = \"UnnamedURL\"\n\t}\n\treturn fs.PutURL(ctx, path, dstName, urlStr)\n}\n"
  },
  {
    "path": "internal/offline_download/tool/base.go",
    "content": "package tool\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype AddUrlArgs struct {\n\tUrl     string\n\tUID     string\n\tTempDir string\n\tSignal  chan int\n}\n\ntype Status struct {\n\tTotalBytes int64\n\tProgress   float64\n\tNewGID     string\n\tCompleted  bool\n\tStatus     string\n\tErr        error\n}\n\ntype Tool interface {\n\tName() string\n\t// Items return the setting items the tool need\n\tItems() []model.SettingItem\n\tInit() (string, error)\n\tIsReady() bool\n\t// AddURL add an uri to download, return the task id\n\tAddURL(args *AddUrlArgs) (string, error)\n\t// Remove the download if task been canceled\n\tRemove(task *DownloadTask) error\n\t// Status return the status of the download task, if an error occurred, return the error in Status.Err\n\tStatus(task *DownloadTask) (*Status, error)\n\n\t// Run for simple http download\n\tRun(task *DownloadTask) error\n}\n"
  },
  {
    "path": "internal/offline_download/tool/download.go",
    "content": "package tool\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task_group\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype DownloadTask struct {\n\ttask.TaskExtension\n\tUrl               string       `json:\"url\"`\n\tDstDirPath        string       `json:\"dst_dir_path\"`\n\tTempDir           string       `json:\"temp_dir\"`\n\tDeletePolicy      DeletePolicy `json:\"delete_policy\"`\n\tToolname          string       `json:\"toolname\"`\n\tStatus            string       `json:\"-\"`\n\tSignal            chan int     `json:\"-\"`\n\tGID               string       `json:\"-\"`\n\ttool              Tool\n\tcallStatusRetried int\n}\n\nfunc (t *DownloadTask) Run() error {\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\tif t.tool == nil {\n\t\ttool, err := Tools.Get(t.Toolname)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed get tool\")\n\t\t}\n\t\tt.tool = tool\n\t}\n\tif err := t.tool.Run(t); !errs.IsNotSupportError(err) {\n\t\tif err == nil {\n\t\t\treturn t.Transfer()\n\t\t}\n\t\treturn err\n\t}\n\tt.Signal = make(chan int)\n\tdefer func() {\n\t\tt.Signal = nil\n\t}()\n\tgid, err := t.tool.AddURL(&AddUrlArgs{\n\t\tUrl:     t.Url,\n\t\tUID:     t.ID,\n\t\tTempDir: t.TempDir,\n\t\tSignal:  t.Signal,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tt.GID = gid\n\tvar ok bool\nouter:\n\tfor {\n\t\tselect {\n\t\tcase <-t.CtxDone():\n\t\t\terr := t.tool.Remove(t)\n\t\t\treturn err\n\t\tcase <-t.Signal:\n\t\t\tok, err = t.Update()\n\t\t\tif ok {\n\t\t\t\tbreak outer\n\t\t\t}\n\t\tcase <-time.After(time.Second * 3):\n\t\t\tok, err = t.Update()\n\t\t\tif ok {\n\t\t\t\tbreak outer\n\t\t\t}\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tif t.tool.Name() == \"Pikpak\" {\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"Thunder\" {\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"ThunderBrowser\" {\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"ThunderX\" {\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"115 Cloud\" {\n\t\t// hack for 115\n\t\t<-time.After(time.Second * 1)\n\t\terr := t.tool.Remove(t)\n\t\tif err != nil {\n\t\t\tlog.Errorln(err.Error())\n\t\t}\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"115 Open\" {\n\t\treturn nil\n\t}\n\tif t.tool.Name() == \"123 Open\" {\n\t\treturn nil\n\t}\n\tt.Status = \"offline download completed, maybe transferring\"\n\t// hack for qBittorrent\n\tif t.tool.Name() == \"qBittorrent\" {\n\t\tseedTime := setting.GetInt(conf.QbittorrentSeedtime, 0)\n\t\tif seedTime >= 0 {\n\t\t\tt.Status = \"offline download completed, waiting for seeding\"\n\t\t\t<-time.After(time.Minute * time.Duration(seedTime))\n\t\t\terr := t.tool.Remove(t)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorln(err.Error())\n\t\t\t}\n\t\t}\n\t}\n\n\tif t.tool.Name() == \"Transmission\" {\n\t\t// hack for transmission\n\t\tseedTime := setting.GetInt(conf.TransmissionSeedtime, 0)\n\t\tif seedTime >= 0 {\n\t\t\tt.Status = \"offline download completed, waiting for seeding\"\n\t\t\t<-time.After(time.Minute * time.Duration(seedTime))\n\t\t\terr := t.tool.Remove(t)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorln(err.Error())\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Update download status, return true if download completed\nfunc (t *DownloadTask) Update() (bool, error) {\n\tinfo, err := t.tool.Status(t)\n\tif err != nil {\n\t\tt.callStatusRetried++\n\t\tlog.Errorf(\"failed to get status of %s, retried %d times\", t.ID, t.callStatusRetried)\n\t\treturn false, nil\n\t}\n\tif t.callStatusRetried > 5 {\n\t\treturn true, errors.Errorf(\"failed to get status of %s, retried %d times\", t.ID, t.callStatusRetried)\n\t}\n\tt.callStatusRetried = 0\n\tt.SetProgress(info.Progress)\n\tt.SetTotalBytes(info.TotalBytes)\n\tt.Status = fmt.Sprintf(\"[%s]: %s\", t.tool.Name(), info.Status)\n\tif info.NewGID != \"\" {\n\t\tlog.Debugf(\"followen by: %+v\", info.NewGID)\n\t\tt.GID = info.NewGID\n\t\treturn false, nil\n\t}\n\t// if download completed\n\tif info.Completed {\n\t\terr := t.Transfer()\n\t\treturn true, errors.WithMessage(err, \"failed to transfer file\")\n\t}\n\t// if download failed\n\tif info.Err != nil {\n\t\treturn true, errors.Errorf(\"failed to download %s, error: %s\", t.ID, info.Err.Error())\n\t}\n\treturn false, nil\n}\n\nfunc (t *DownloadTask) Transfer() error {\n\ttoolName := t.tool.Name()\n\tif toolName == \"115 Cloud\" || toolName == \"115 Open\" || toolName == \"123 Open\" || toolName == \"123Pan\" || toolName == \"PikPak\" || toolName == \"Thunder\" || toolName == \"ThunderX\" || toolName == \"ThunderBrowser\" {\n\t\t// 如果不是直接下载到目标路径，则进行转存\n\t\tif t.TempDir != t.DstDirPath {\n\t\t\treturn transferObj(t.Ctx(), t.TempDir, t.DstDirPath, t.DeletePolicy)\n\t\t}\n\t\treturn nil\n\t}\n\tif t.DeletePolicy == UploadDownloadStream {\n\t\tdstStorage, dstDirActualPath, err := op.GetStorageAndActualPath(t.DstDirPath)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed get dst storage\")\n\t\t}\n\t\ttaskCreator, _ := t.Ctx().Value(conf.UserKey).(*model.User)\n\t\ttsk := &TransferTask{\n\t\t\tTaskData: fs.TaskData{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: taskCreator,\n\t\t\t\t\tApiUrl:  t.ApiUrl,\n\t\t\t\t},\n\t\t\t\tSrcActualPath: t.TempDir,\n\t\t\t\tDstActualPath: dstDirActualPath,\n\t\t\t\tDstStorage:    dstStorage,\n\t\t\t\tDstStorageMp:  dstStorage.GetStorage().MountPath,\n\t\t\t},\n\t\t\tDeletePolicy: t.DeletePolicy,\n\t\t\tUrl:          t.Url,\n\t\t}\n\t\ttsk.SetTotalBytes(t.GetTotalBytes())\n\t\ttsk.groupID = path.Join(tsk.DstStorageMp, tsk.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(tsk.groupID, nil)\n\t\tTransferTaskManager.Add(tsk)\n\t\treturn nil\n\t}\n\treturn transferStd(t.Ctx(), t.TempDir, t.DstDirPath, t.DeletePolicy)\n}\n\nfunc (t *DownloadTask) GetName() string {\n\treturn fmt.Sprintf(\"download %s to (%s)\", t.Url, t.DstDirPath)\n}\n\nfunc (t *DownloadTask) GetStatus() string {\n\treturn t.Status\n}\n\nvar DownloadTaskManager *tache.Manager[*DownloadTask]\n"
  },
  {
    "path": "internal/offline_download/tool/tools.go",
    "content": "package tool\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\nvar (\n\tTools = make(ToolsManager)\n)\n\ntype ToolsManager map[string]Tool\n\nfunc (t ToolsManager) Get(name string) (Tool, error) {\n\tif tool, ok := t[name]; ok {\n\t\treturn tool, nil\n\t}\n\treturn nil, fmt.Errorf(\"tool %s not found\", name)\n}\n\nfunc (t ToolsManager) Add(tool Tool) {\n\tt[tool.Name()] = tool\n}\n\nfunc (t ToolsManager) Names() []string {\n\tnames := make([]string, 0, len(t))\n\tfor name := range t {\n\t\tif tool, err := t.Get(name); err == nil && tool.IsReady() {\n\t\t\tnames = append(names, name)\n\t\t}\n\t}\n\tsort.Strings(names)\n\treturn names\n}\n\nfunc (t ToolsManager) Items() []model.SettingItem {\n\tvar items []model.SettingItem\n\tfor _, tool := range t {\n\t\titems = append(items, tool.Items()...)\n\t}\n\treturn items\n}\n"
  },
  {
    "path": "internal/offline_download/tool/transfer.go",
    "content": "package tool\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task_group\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype TransferTask struct {\n\tfs.TaskData\n\tDeletePolicy DeletePolicy `json:\"delete_policy\"`\n\tUrl          string       `json:\"url\"`\n\tgroupID      string       `json:\"-\"`\n}\n\nfunc (t *TransferTask) Run() error {\n\tif t.SrcStorage == nil && t.SrcStorageMp != \"\" {\n\t\tif srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {\n\t\t\tt.SrcStorage = srcStorage\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t\tif t.DstStorage == nil {\n\t\t\tif dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {\n\t\t\t\tt.DstStorage = dstStorage\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\tif t.SrcStorage == nil {\n\t\tif t.DeletePolicy == UploadDownloadStream {\n\t\t\trr, err := stream.GetRangeReaderFromLink(t.GetTotalBytes(), &model.Link{URL: t.Url})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tr, err := rr.RangeRead(t.Ctx(), http_range.Range{Length: t.GetTotalBytes()})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tname := t.SrcActualPath\n\t\t\tmimetype := utils.GetMimeType(name)\n\t\t\ts := &stream.FileStream{\n\t\t\t\tCtx: t.Ctx(),\n\t\t\t\tObj: &model.Object{\n\t\t\t\t\tName:     name,\n\t\t\t\t\tSize:     t.GetTotalBytes(),\n\t\t\t\t\tModified: time.Now(),\n\t\t\t\t\tIsFolder: false,\n\t\t\t\t},\n\t\t\t\tReader:   r,\n\t\t\t\tMimetype: mimetype,\n\t\t\t\tClosers:  utils.NewClosers(r),\n\t\t\t}\n\t\t\treturn op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.DstStorage, t.DstActualPath, s, t.SetProgress)\n\t\t}\n\t\treturn transferStdPath(t)\n\t}\n\treturn transferObjPath(t)\n}\n\nfunc (t *TransferTask) GetName() string {\n\tif t.DeletePolicy == UploadDownloadStream {\n\t\treturn fmt.Sprintf(\"upload [%s](%s) to [%s](%s)\", t.SrcActualPath, t.Url, t.DstStorageMp, t.DstActualPath)\n\t}\n\treturn fmt.Sprintf(\"transfer [%s](%s) to [%s](%s)\", t.SrcStorageMp, t.SrcActualPath, t.DstStorageMp, t.DstActualPath)\n}\n\nfunc (t *TransferTask) OnSucceeded() {\n\tif t.DeletePolicy == DeleteOnUploadSucceed || t.DeletePolicy == DeleteAlways {\n\t\tif t.SrcStorage == nil {\n\t\t\tremoveStdTemp(t)\n\t\t} else {\n\t\t\tremoveObjTemp(t)\n\t\t}\n\t}\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, true)\n}\n\nfunc (t *TransferTask) OnFailed() {\n\tif t.DeletePolicy == DeleteOnUploadFailed || t.DeletePolicy == DeleteAlways {\n\t\tif t.SrcStorage == nil {\n\t\t\tremoveStdTemp(t)\n\t\t} else {\n\t\t\tremoveObjTemp(t)\n\t\t}\n\t}\n\ttask_group.TransferCoordinator.Done(context.WithoutCancel(t.Ctx()), t.groupID, false)\n}\n\nfunc (t *TransferTask) SetRetry(retry int, maxRetry int) {\n\tif retry == 0 &&\n\t\t(len(t.groupID) == 0 || // 重启恢复\n\t\t\t(t.GetErr() == nil && t.GetState() != tache.StatePending)) { // 手动重试\n\t\tt.groupID = stdpath.Join(t.DstStorageMp, t.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t}\n\tt.TaskData.SetRetry(retry, maxRetry)\n}\n\nvar (\n\tTransferTaskManager *tache.Manager[*TransferTask]\n)\n\nfunc transferStd(ctx context.Context, tempDir, dstDirPath string, deletePolicy DeletePolicy) error {\n\tdstStorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get dst storage\")\n\t}\n\tentries, err := os.ReadDir(tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttaskCreator, _ := ctx.Value(conf.UserKey).(*model.User)\n\tfor _, entry := range entries {\n\t\tt := &TransferTask{\n\t\t\tTaskData: fs.TaskData{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: taskCreator,\n\t\t\t\t\tApiUrl:  common.GetApiUrl(ctx),\n\t\t\t\t},\n\t\t\t\tSrcActualPath: stdpath.Join(tempDir, entry.Name()),\n\t\t\t\tDstActualPath: dstDirActualPath,\n\t\t\t\tDstStorage:    dstStorage,\n\t\t\t\tDstStorageMp:  dstStorage.GetStorage().MountPath,\n\t\t\t},\n\t\t\tDeletePolicy: deletePolicy,\n\t\t}\n\t\tt.groupID = path.Join(t.DstStorageMp, t.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\tTransferTaskManager.Add(t)\n\t}\n\treturn nil\n}\n\nfunc transferStdPath(t *TransferTask) error {\n\tt.Status = \"getting src object\"\n\tinfo, err := os.Stat(t.SrcActualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.IsDir() {\n\t\tt.Status = \"src object is dir, listing objs\"\n\t\tentries, err := os.ReadDir(t.SrcActualPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdstDirActualPath := stdpath.Join(t.DstActualPath, info.Name())\n\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(dstDirActualPath))\n\t\tfor _, entry := range entries {\n\t\t\tsrcRawPath := stdpath.Join(t.SrcActualPath, entry.Name())\n\t\t\ttask := &TransferTask{\n\t\t\t\tTaskData: fs.TaskData{\n\t\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t\t\tApiUrl:  t.ApiUrl,\n\t\t\t\t\t},\n\t\t\t\t\tSrcActualPath: srcRawPath,\n\t\t\t\t\tDstActualPath: dstDirActualPath,\n\t\t\t\t\tDstStorage:    t.DstStorage,\n\t\t\t\t\tSrcStorageMp:  t.SrcStorageMp,\n\t\t\t\t\tDstStorageMp:  t.DstStorageMp,\n\t\t\t\t},\n\t\t\t\tgroupID:      t.groupID,\n\t\t\t\tDeletePolicy: t.DeletePolicy,\n\t\t\t}\n\t\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\t\tTransferTaskManager.Add(task)\n\t\t}\n\t\tt.Status = \"src object is dir, added all transfer tasks of files\"\n\t\treturn nil\n\t}\n\treturn transferStdFile(t)\n}\n\nfunc transferStdFile(t *TransferTask) error {\n\trc, err := os.Open(t.SrcActualPath)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to open file %s\", t.SrcActualPath)\n\t}\n\tinfo, err := rc.Stat()\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to get file %s\", t.SrcActualPath)\n\t}\n\tmimetype := utils.GetMimeType(t.SrcActualPath)\n\ts := &stream.FileStream{\n\t\tCtx: t.Ctx(),\n\t\tObj: &model.Object{\n\t\t\tName:     filepath.Base(t.SrcActualPath),\n\t\t\tSize:     info.Size(),\n\t\t\tModified: info.ModTime(),\n\t\t\tIsFolder: false,\n\t\t},\n\t\tReader:   rc,\n\t\tMimetype: mimetype,\n\t\tClosers:  utils.NewClosers(rc),\n\t}\n\tt.SetTotalBytes(info.Size())\n\treturn op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.DstStorage, t.DstActualPath, s, t.SetProgress)\n}\n\nfunc removeStdTemp(t *TransferTask) {\n\tinfo, err := os.Stat(t.SrcActualPath)\n\tif err != nil || info.IsDir() {\n\t\treturn\n\t}\n\tif err := os.Remove(t.SrcActualPath); err != nil {\n\t\tlog.Errorf(\"failed to delete temp file %s, error: %s\", t.SrcActualPath, err.Error())\n\t}\n}\n\nfunc transferObj(ctx context.Context, tempDir, dstDirPath string, deletePolicy DeletePolicy) error {\n\tsrcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(tempDir)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get src storage\")\n\t}\n\tdstStorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get dst storage\")\n\t}\n\tobjs, err := op.List(ctx, srcStorage, srcObjActualPath, model.ListArgs{})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", tempDir)\n\t}\n\ttaskCreator, _ := ctx.Value(conf.UserKey).(*model.User) // taskCreator is nil when convert failed\n\tfor _, obj := range objs {\n\t\tt := &TransferTask{\n\t\t\tTaskData: fs.TaskData{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: taskCreator,\n\t\t\t\t\tApiUrl:  common.GetApiUrl(ctx),\n\t\t\t\t},\n\t\t\t\tSrcActualPath: stdpath.Join(srcObjActualPath, obj.GetName()),\n\t\t\t\tDstActualPath: dstDirActualPath,\n\t\t\t\tSrcStorage:    srcStorage,\n\t\t\t\tDstStorage:    dstStorage,\n\t\t\t\tSrcStorageMp:  srcStorage.GetStorage().MountPath,\n\t\t\t\tDstStorageMp:  dstStorage.GetStorage().MountPath,\n\t\t\t},\n\t\t\tDeletePolicy: deletePolicy,\n\t\t}\n\t\tt.groupID = path.Join(t.DstStorageMp, t.DstActualPath)\n\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\tTransferTaskManager.Add(t)\n\t}\n\treturn nil\n}\n\nfunc transferObjPath(t *TransferTask) error {\n\tt.Status = \"getting src object\"\n\tsrcObj, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcActualPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", t.SrcActualPath)\n\t}\n\tif srcObj.IsDir() {\n\t\tt.Status = \"src object is dir, listing objs\"\n\t\tobjs, err := op.List(t.Ctx(), t.SrcStorage, t.SrcActualPath, model.ListArgs{})\n\t\tif err != nil {\n\t\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", t.SrcActualPath)\n\t\t}\n\t\tdstDirActualPath := stdpath.Join(t.DstActualPath, srcObj.GetName())\n\t\ttask_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(dstDirActualPath))\n\t\tfor _, obj := range objs {\n\t\t\tif utils.IsCanceled(t.Ctx()) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tsrcObjPath := stdpath.Join(t.SrcActualPath, obj.GetName())\n\t\t\ttask_group.TransferCoordinator.AddTask(t.groupID, nil)\n\t\t\tTransferTaskManager.Add(&TransferTask{\n\t\t\t\tTaskData: fs.TaskData{\n\t\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t\t\tApiUrl:  t.ApiUrl,\n\t\t\t\t\t},\n\t\t\t\t\tSrcActualPath: srcObjPath,\n\t\t\t\t\tDstActualPath: dstDirActualPath,\n\t\t\t\t\tSrcStorage:    t.SrcStorage,\n\t\t\t\t\tDstStorage:    t.DstStorage,\n\t\t\t\t\tSrcStorageMp:  t.SrcStorageMp,\n\t\t\t\t\tDstStorageMp:  t.DstStorageMp,\n\t\t\t\t},\n\t\t\t\tgroupID:      t.groupID,\n\t\t\t\tDeletePolicy: t.DeletePolicy,\n\t\t\t})\n\t\t}\n\t\tt.Status = \"src object is dir, added all transfer tasks of objs\"\n\t\treturn nil\n\t}\n\treturn transferObjFile(t)\n}\n\nfunc transferObjFile(t *TransferTask) error {\n\t_, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcActualPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", t.SrcActualPath)\n\t}\n\tlink, srcFile, err := op.Link(t.Ctx(), t.SrcStorage, t.SrcActualPath, model.LinkArgs{})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] link\", t.SrcActualPath)\n\t}\n\t// any link provided is seekable\n\tss, err := stream.NewSeekableStream(&stream.FileStream{\n\t\tObj: srcFile,\n\t\tCtx: t.Ctx(),\n\t}, link)\n\tif err != nil {\n\t\t_ = link.Close()\n\t\treturn errors.WithMessagef(err, \"failed get [%s] stream\", t.SrcActualPath)\n\t}\n\tt.SetTotalBytes(ss.GetSize())\n\treturn op.Put(context.WithValue(t.Ctx(), conf.SkipHookKey, struct{}{}), t.DstStorage, t.DstActualPath, ss, t.SetProgress)\n}\n\nfunc removeObjTemp(t *TransferTask) {\n\tsrcObj, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcActualPath)\n\tif err != nil || srcObj.IsDir() {\n\t\treturn\n\t}\n\tif err := op.Remove(t.Ctx(), t.SrcStorage, t.SrcActualPath); err != nil {\n\t\tlog.Errorf(\"failed to delete temp obj %s, error: %s\", t.SrcActualPath, err.Error())\n\t}\n}\n"
  },
  {
    "path": "internal/offline_download/transmission/client.go",
    "content": "package transmission\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/hekmon/transmissionrpc/v3\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Transmission struct {\n\tclient *transmissionrpc.Client\n}\n\nfunc (t *Transmission) Run(task *tool.DownloadTask) error {\n\treturn errs.NotSupport\n}\n\nfunc (t *Transmission) Name() string {\n\treturn \"Transmission\"\n}\n\nfunc (t *Transmission) Items() []model.SettingItem {\n\t// transmission settings\n\treturn []model.SettingItem{\n\t\t{Key: conf.TransmissionUri, Value: \"http://localhost:9091/transmission/rpc\", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.TransmissionSeedtime, Value: \"0\", Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n}\n\nfunc (t *Transmission) Init() (string, error) {\n\tt.client = nil\n\turi := setting.GetStr(conf.TransmissionUri)\n\tendpoint, err := url.Parse(uri)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to init transmission client\")\n\t}\n\tc, err := transmissionrpc.New(endpoint, nil)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to init transmission client\")\n\t}\n\n\tok, serverVersion, serverMinimumVersion, err := c.RPCVersion(context.Background())\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed get transmission version\")\n\t}\n\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"remote transmission RPC version (v%d) is incompatible with the transmission library (v%d): remote needs at least v%d\",\n\t\t\tserverVersion, transmissionrpc.RPCVersion, serverMinimumVersion)\n\t}\n\n\tt.client = c\n\tlog.Infof(\"remote transmission RPC version (v%d) is compatible with our transmissionrpc library (v%d)\\n\",\n\t\tserverVersion, transmissionrpc.RPCVersion)\n\tlog.Infof(\"using transmission version: %d\", serverVersion)\n\treturn fmt.Sprintf(\"transmission version: %d\", serverVersion), nil\n}\n\nfunc (t *Transmission) IsReady() bool {\n\treturn t.client != nil\n}\n\nfunc (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) {\n\tendpoint, err := url.Parse(args.Url)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to parse transmission uri\")\n\t}\n\n\trpcPayload := transmissionrpc.TorrentAddPayload{\n\t\tDownloadDir: &args.TempDir,\n\t}\n\t// http url for .torrent file\n\tif endpoint.Scheme == \"http\" || endpoint.Scheme == \"https\" {\n\t\tresp, err := http.Get(args.Url)\n\t\tif err != nil {\n\t\t\treturn \"\", errors.Wrap(err, \"failed to get .torrent file\")\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tbuffer := new(bytes.Buffer)\n\t\tencoder := base64.NewEncoder(base64.StdEncoding, buffer)\n\t\t// Stream file to the encoder\n\t\tif _, err = utils.CopyWithBuffer(encoder, resp.Body); err != nil {\n\t\t\treturn \"\", errors.Wrap(err, \"can't copy file content into the base64 encoder\")\n\t\t}\n\t\t// Flush last bytes\n\t\tif err = encoder.Close(); err != nil {\n\t\t\treturn \"\", errors.Wrap(err, \"can't flush last bytes of the base64 encoder\")\n\t\t}\n\t\t// Get the string form\n\t\tb64 := buffer.String()\n\t\trpcPayload.MetaInfo = &b64\n\t} else { // magnet uri\n\t\trpcPayload.Filename = &args.Url\n\t}\n\n\ttorrent, err := t.client.TorrentAdd(context.TODO(), rpcPayload)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif torrent.ID == nil {\n\t\treturn \"\", fmt.Errorf(\"failed get torrent ID\")\n\t}\n\tgid := strconv.FormatInt(*torrent.ID, 10)\n\treturn gid, nil\n}\n\nfunc (t *Transmission) Remove(task *tool.DownloadTask) error {\n\tgid, err := strconv.ParseInt(task.GID, 10, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = t.client.TorrentRemove(context.TODO(), transmissionrpc.TorrentRemovePayload{\n\t\tIDs:             []int64{gid},\n\t\tDeleteLocalData: false,\n\t})\n\treturn err\n}\n\nfunc (t *Transmission) Status(task *tool.DownloadTask) (*tool.Status, error) {\n\tgid, err := strconv.ParseInt(task.GID, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfos, err := t.client.TorrentGetAllFor(context.TODO(), []int64{gid})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(infos) < 1 {\n\t\treturn nil, fmt.Errorf(\"failed get status, wrong gid: %s\", task.GID)\n\t}\n\tinfo := infos[0]\n\n\ts := &tool.Status{\n\t\tCompleted: *info.IsFinished,\n\t\tErr:       err,\n\t}\n\ts.Progress = *info.PercentDone * 100\n\ts.TotalBytes = int64(*info.SizeWhenDone / 8)\n\n\tswitch *info.Status {\n\tcase transmissionrpc.TorrentStatusCheckWait,\n\t\ttransmissionrpc.TorrentStatusDownloadWait,\n\t\ttransmissionrpc.TorrentStatusCheck,\n\t\ttransmissionrpc.TorrentStatusDownload,\n\t\ttransmissionrpc.TorrentStatusIsolated:\n\t\ts.Status = \"[transmission] \" + info.Status.String()\n\tcase transmissionrpc.TorrentStatusSeedWait,\n\t\ttransmissionrpc.TorrentStatusSeed:\n\t\ts.Completed = true\n\tcase transmissionrpc.TorrentStatusStopped:\n\t\ts.Err = errors.Errorf(\"[transmission] failed to download %s, status: %s, error: %s\", task.GID, info.Status.String(), *info.ErrorString)\n\tdefault:\n\t\ts.Err = errors.Errorf(\"[transmission] unknown status occurred downloading %s, err: %s\", task.GID, *info.ErrorString)\n\t}\n\treturn s, nil\n}\n\nvar _ tool.Tool = (*Transmission)(nil)\n\nfunc init() {\n\ttool.Tools.Add(&Transmission{})\n}\n"
  },
  {
    "path": "internal/op/archive.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/cache\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tgocache \"github.com/OpenListTeam/go-cache\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/time/rate\"\n)\n\nvar (\n\tarchiveMetaCache = gocache.NewMemCache(gocache.WithShards[*model.ArchiveMetaProvider](64))\n\tarchiveMetaG     singleflight.Group[*model.ArchiveMetaProvider]\n)\n\nfunc GetArchiveMeta(ctx context.Context, storage driver.Driver, path string, args model.ArchiveMetaArgs) (*model.ArchiveMetaProvider, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tkey := Key(storage, path)\n\tfn := func() (*model.ArchiveMetaProvider, error) {\n\t\t_, m, err := getArchiveMeta(ctx, storage, path, args)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed to get %s archive met: %+v\", path, err)\n\t\t}\n\t\tif m.Expiration != nil {\n\t\t\tarchiveMetaCache.Set(key, m, gocache.WithEx[*model.ArchiveMetaProvider](*m.Expiration))\n\t\t}\n\t\treturn m, nil\n\t}\n\t// if storage.Config().NoLinkSingleflight {\n\t// \tmeta, err := fn()\n\t// \treturn meta, err\n\t// }\n\tif !args.Refresh {\n\t\tif meta, ok := archiveMetaCache.Get(key); ok {\n\t\t\tlog.Debugf(\"use cache when get %s archive meta\", path)\n\t\t\treturn meta, nil\n\t\t}\n\t}\n\tmeta, err, _ := archiveMetaG.Do(key, fn)\n\treturn meta, err\n}\n\nfunc GetArchiveToolAndStream(ctx context.Context, storage driver.Driver, path string, args model.LinkArgs) (model.Obj, tool.Tool, []*stream.SeekableStream, error) {\n\tl, obj, err := Link(ctx, storage, path, args)\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.WithMessagef(err, \"failed get [%s] link\", path)\n\t}\n\n\t// Get archive tool\n\tvar partExt *tool.MultipartExtension\n\tvar t tool.Tool\n\text := obj.GetName()\n\tfor {\n\t\tvar found bool\n\t\t_, ext, found = strings.Cut(ext, \".\")\n\t\tif !found {\n\t\t\t_ = l.Close()\n\t\t\treturn nil, nil, nil, errors.Errorf(\"failed get archive tool: the obj does not have an extension.\")\n\t\t}\n\t\tpartExt, t, err = tool.GetArchiveTool(\".\" + ext)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Get first part stream\n\tss, err := stream.NewSeekableStream(&stream.FileStream{Ctx: ctx, Obj: obj}, l)\n\tif err != nil {\n\t\t_ = l.Close()\n\t\treturn nil, nil, nil, errors.WithMessagef(err, \"failed get [%s] stream\", path)\n\t}\n\tret := []*stream.SeekableStream{ss}\n\tif partExt == nil {\n\t\treturn obj, t, ret, nil\n\t}\n\n\t// Merge multi-part archive\n\tdir := stdpath.Dir(path)\n\tobjs, err := List(ctx, storage, dir, model.ListArgs{})\n\tif err != nil {\n\t\treturn obj, t, ret, nil\n\t}\n\tfor _, o := range objs {\n\t\tsubmatch := partExt.PartFileFormat.FindStringSubmatch(o.GetName())\n\t\tif submatch == nil {\n\t\t\tcontinue\n\t\t}\n\t\tpartIdx, e := strconv.Atoi(submatch[1])\n\t\tif e != nil {\n\t\t\tcontinue\n\t\t}\n\t\tpartIdx = partIdx - partExt.SecondPartIndex + 1\n\t\tif partIdx < 1 {\n\t\t\tcontinue\n\t\t}\n\t\tp := stdpath.Join(dir, o.GetName())\n\t\tl1, o1, e := Link(ctx, storage, p, args)\n\t\tif e != nil {\n\t\t\terr = errors.WithMessagef(e, \"failed get [%s] link\", p)\n\t\t\tbreak\n\t\t}\n\t\tss1, e := stream.NewSeekableStream(&stream.FileStream{Ctx: ctx, Obj: o1}, l1)\n\t\tif e != nil {\n\t\t\t_ = l1.Close()\n\t\t\terr = errors.WithMessagef(e, \"failed get [%s] stream\", p)\n\t\t\tbreak\n\t\t}\n\t\tfor partIdx >= len(ret) {\n\t\t\tret = append(ret, nil)\n\t\t}\n\t\tret[partIdx] = ss1\n\t}\n\tcloseAll := func(r []*stream.SeekableStream) {\n\t\tfor _, s := range r {\n\t\t\tif s != nil {\n\t\t\t\t_ = s.Close()\n\t\t\t}\n\t\t}\n\t}\n\tif err != nil {\n\t\tcloseAll(ret)\n\t\treturn nil, nil, nil, err\n\t}\n\tfor i, ss1 := range ret {\n\t\tif ss1 == nil {\n\t\t\tcloseAll(ret)\n\t\t\treturn nil, nil, nil, errors.Errorf(\"failed merge [%s] parts, missing part %d\", path, i)\n\t\t}\n\t}\n\treturn obj, t, ret, nil\n}\n\nfunc getArchiveMeta(ctx context.Context, storage driver.Driver, path string, args model.ArchiveMetaArgs) (model.Obj, *model.ArchiveMetaProvider, error) {\n\tstorageAr, ok := storage.(driver.ArchiveReader)\n\tif ok {\n\t\tobj, err := GetUnwrap(ctx, storage, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed to get file\")\n\t\t}\n\t\tif obj.IsDir() {\n\t\t\treturn nil, nil, errors.WithStack(errs.NotFile)\n\t\t}\n\t\tmeta, err := storageAr.GetArchiveMeta(ctx, obj, args.ArchiveArgs)\n\t\tif !errors.Is(err, errs.NotImplement) {\n\t\t\tarchiveMetaProvider := &model.ArchiveMetaProvider{ArchiveMeta: meta, DriverProviding: true}\n\t\t\tif meta != nil && meta.GetTree() != nil {\n\t\t\t\tarchiveMetaProvider.Sort = &storage.GetStorage().Sort\n\t\t\t}\n\t\t\tif !storage.Config().NoCache {\n\t\t\t\tExpiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)\n\t\t\t\tarchiveMetaProvider.Expiration = &Expiration\n\t\t\t}\n\t\t\treturn obj, archiveMetaProvider, err\n\t\t}\n\t}\n\tobj, t, ss, err := GetArchiveToolAndStream(ctx, storage, path, args.LinkArgs)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer func() {\n\t\tvar e error\n\t\tfor _, s := range ss {\n\t\t\te = stderrors.Join(e, s.Close())\n\t\t}\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", e)\n\t\t}\n\t}()\n\tmeta, err := t.GetMeta(ss, args.ArchiveArgs)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tarchiveMetaProvider := &model.ArchiveMetaProvider{ArchiveMeta: meta, DriverProviding: false}\n\tif meta.GetTree() != nil {\n\t\tarchiveMetaProvider.Sort = &storage.GetStorage().Sort\n\t}\n\tif !storage.Config().NoCache {\n\t\tExpiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)\n\t\tarchiveMetaProvider.Expiration = &Expiration\n\t}\n\treturn obj, archiveMetaProvider, err\n}\n\nvar (\n\tarchiveListCache = gocache.NewMemCache(gocache.WithShards[[]model.Obj](64))\n\tarchiveListG     singleflight.Group[[]model.Obj]\n)\n\nfunc ListArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) ([]model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tmetaKey := Key(storage, path)\n\tkey := stdpath.Join(metaKey, args.InnerPath)\n\tif !args.Refresh {\n\t\tif files, ok := archiveListCache.Get(key); ok {\n\t\t\tlog.Debugf(\"use cache when list archive [%s]%s\", path, args.InnerPath)\n\t\t\treturn files, nil\n\t\t}\n\t\t// if meta, ok := archiveMetaCache.Get(metaKey); ok {\n\t\t// \tlog.Debugf(\"use meta cache when list archive [%s]%s\", path, args.InnerPath)\n\t\t// \treturn getChildrenFromArchiveMeta(meta, args.InnerPath)\n\t\t// }\n\t}\n\tobjs, err, _ := archiveListG.Do(key, func() ([]model.Obj, error) {\n\t\tfiles, err := listArchive(ctx, storage, path, args)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed to list archive [%s]%s: %+v\", path, args.InnerPath, err)\n\t\t}\n\t\t// warp obj name\n\t\tmodel.WrapObjsName(files)\n\t\t// sort objs\n\t\tif storage.Config().LocalSort {\n\t\t\tmodel.SortFiles(files, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)\n\t\t}\n\t\tmodel.ExtractFolder(files, storage.GetStorage().ExtractFolder)\n\t\tif !storage.Config().NoCache {\n\t\t\tif len(files) > 0 {\n\t\t\t\tlog.Debugf(\"set cache: %s => %+v\", key, files)\n\t\t\t\tarchiveListCache.Set(key, files, gocache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"del cache: %s\", key)\n\t\t\t\tarchiveListCache.Del(key)\n\t\t\t}\n\t\t}\n\t\treturn files, nil\n\t})\n\treturn objs, err\n}\n\nfunc _listArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) ([]model.Obj, error) {\n\tstorageAr, ok := storage.(driver.ArchiveReader)\n\tif ok {\n\t\tobj, err := GetUnwrap(ctx, storage, path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed to get file\")\n\t\t}\n\t\tif obj.IsDir() {\n\t\t\treturn nil, errors.WithStack(errs.NotFile)\n\t\t}\n\t\tfiles, err := storageAr.ListArchive(ctx, obj, args.ArchiveInnerArgs)\n\t\tif !errors.Is(err, errs.NotImplement) {\n\t\t\treturn files, err\n\t\t}\n\t}\n\t_, t, ss, err := GetArchiveToolAndStream(ctx, storage, path, args.LinkArgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tvar e error\n\t\tfor _, s := range ss {\n\t\t\te = stderrors.Join(e, s.Close())\n\t\t}\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", e)\n\t\t}\n\t}()\n\tfiles, err := t.List(ss, args.ArchiveInnerArgs)\n\treturn files, err\n}\n\nfunc listArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) ([]model.Obj, error) {\n\tfiles, err := _listArchive(ctx, storage, path, args)\n\tif errors.Is(err, errs.NotSupport) {\n\t\tvar meta model.ArchiveMeta\n\t\tmeta, err = GetArchiveMeta(ctx, storage, path, model.ArchiveMetaArgs{\n\t\t\tArchiveArgs: args.ArchiveArgs,\n\t\t\tRefresh:     args.Refresh,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfiles, err = getChildrenFromArchiveMeta(meta, args.InnerPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn files, err\n}\n\nfunc getChildrenFromArchiveMeta(meta model.ArchiveMeta, innerPath string) ([]model.Obj, error) {\n\tobj := meta.GetTree()\n\tif obj == nil {\n\t\treturn nil, errors.WithStack(errs.NotImplement)\n\t}\n\tdirs := splitPath(innerPath)\n\tfor _, dir := range dirs {\n\t\tvar next model.ObjTree\n\t\tfor _, c := range obj {\n\t\t\tif c.GetName() == dir {\n\t\t\t\tnext = c\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif next == nil {\n\t\t\treturn nil, errors.WithStack(errs.ObjectNotFound)\n\t\t}\n\t\tif !next.IsDir() || next.GetChildren() == nil {\n\t\t\treturn nil, errors.WithStack(errs.NotFolder)\n\t\t}\n\t\tobj = next.GetChildren()\n\t}\n\treturn utils.SliceConvert(obj, func(src model.ObjTree) (model.Obj, error) {\n\t\treturn src, nil\n\t})\n}\n\nfunc splitPath(path string) []string {\n\tvar parts []string\n\tfor {\n\t\tdir, file := stdpath.Split(path)\n\t\tif file == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tparts = append([]string{file}, parts...)\n\t\tpath = strings.TrimSuffix(dir, \"/\")\n\t}\n\treturn parts\n}\n\nfunc ArchiveGet(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\taf, err := GetUnwrap(ctx, storage, path)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithMessage(err, \"failed to get file\")\n\t}\n\tif af.IsDir() {\n\t\treturn nil, nil, errors.WithStack(errs.NotFile)\n\t}\n\tif g, ok := storage.(driver.ArchiveGetter); ok {\n\t\tobj, err := g.ArchiveGet(ctx, af, args.ArchiveInnerArgs)\n\t\tif err == nil {\n\t\t\treturn af, model.WrapObjName(obj), nil\n\t\t}\n\t}\n\n\tif utils.PathEqual(args.InnerPath, \"/\") {\n\t\treturn af, &model.ObjWrapName{\n\t\t\tName: RootName,\n\t\t\tObj: &model.Object{\n\t\t\t\tName:     af.GetName(),\n\t\t\t\tPath:     af.GetPath(),\n\t\t\t\tID:       af.GetID(),\n\t\t\t\tSize:     af.GetSize(),\n\t\t\t\tModified: af.ModTime(),\n\t\t\t\tIsFolder: true,\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tinnerDir, name := stdpath.Split(args.InnerPath)\n\targs.InnerPath = strings.TrimSuffix(innerDir, \"/\")\n\tfiles, err := ListArchive(ctx, storage, path, args)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithMessage(err, \"failed get parent list\")\n\t}\n\tfor _, f := range files {\n\t\tif f.GetName() == name {\n\t\t\treturn af, f, nil\n\t\t}\n\t}\n\treturn nil, nil, errors.WithStack(errs.ObjectNotFound)\n}\n\ntype objWithLink struct {\n\tlink *model.Link\n\tobj  model.Obj\n}\n\nvar (\n\textractCache = cache.NewKeyedCache[*objWithLink](5 * time.Minute)\n\textractG     = singleflight.Group[*objWithLink]{}\n)\n\nfunc DriverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*model.Link, model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tkey := stdpath.Join(Key(storage, path), args.InnerPath)\n\tif ol, ok := extractCache.Get(key); ok {\n\t\tif ol.link.Expiration != nil || ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {\n\t\t\treturn ol.link, ol.obj, nil\n\t\t}\n\t}\n\n\tfn := func() (*objWithLink, error) {\n\t\tol, err := driverExtract(ctx, storage, path, args)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed extract archive\")\n\t\t}\n\t\tif ol.link.Expiration != nil {\n\t\t\textractCache.SetWithTTL(key, ol, *ol.link.Expiration)\n\t\t} else {\n\t\t\textractCache.SetWithExpirable(key, ol, &ol.link.SyncClosers)\n\t\t}\n\t\treturn ol, nil\n\t}\n\n\tfor {\n\t\tol, err, _ := extractG.Do(key, fn)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tif ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {\n\t\t\treturn ol.link, ol.obj, nil\n\t\t}\n\t}\n}\n\nfunc driverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*objWithLink, error) {\n\tstorageAr, ok := storage.(driver.ArchiveReader)\n\tif !ok {\n\t\treturn nil, errs.DriverExtractNotSupported\n\t}\n\tarchiveFile, extracted, err := ArchiveGet(ctx, storage, path, model.ArchiveListArgs{\n\t\tArchiveInnerArgs: args,\n\t\tRefresh:          false,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed to get file\")\n\t}\n\tif extracted.IsDir() {\n\t\treturn nil, errors.WithStack(errs.NotFile)\n\t}\n\tlink, err := storageAr.Extract(ctx, archiveFile, args)\n\treturn &objWithLink{link: link, obj: extracted}, err\n}\n\ntype streamWithParent struct {\n\trc      io.ReadCloser\n\tparents []*stream.SeekableStream\n}\n\nfunc (s *streamWithParent) Read(p []byte) (int, error) {\n\treturn s.rc.Read(p)\n}\n\nfunc (s *streamWithParent) Close() error {\n\terr := s.rc.Close()\n\tfor _, ss := range s.parents {\n\t\terr = stderrors.Join(err, ss.Close())\n\t}\n\treturn err\n}\n\nfunc InternalExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\t_, t, ss, err := GetArchiveToolAndStream(ctx, storage, path, args.LinkArgs)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\trc, size, err := t.Extract(ss, args)\n\tif err != nil {\n\t\tvar e error\n\t\tfor _, s := range ss {\n\t\t\te = stderrors.Join(e, s.Close())\n\t\t}\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", e)\n\t\t\terr = stderrors.Join(err, e)\n\t\t}\n\t\treturn nil, 0, err\n\t}\n\treturn &streamWithParent{rc: rc, parents: ss}, size, nil\n}\n\nfunc ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tsrcObj, err := GetUnwrap(ctx, storage, srcPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get dst dir\")\n\t}\n\n\tvar newObjs []model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.ArchiveDecompressResult:\n\t\tnewObjs, err = s.ArchiveDecompress(ctx, srcObj, dstDir, args)\n\t\tif err == nil {\n\t\t\tif len(newObjs) > 0 {\n\t\t\t\tif !storage.Config().NoCache {\n\t\t\t\t\tif cache, exist := Cache.dirCache.Get(Key(storage, dstDirPath)); exist {\n\t\t\t\t\t\tfor _, newObj := range newObjs {\n\t\t\t\t\t\t\tcache.UpdateObject(newObj.GetName(), newObj)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tCache.DeleteDirectory(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.ArchiveDecompress:\n\t\terr = s.ArchiveDecompress(ctx, srcObj, dstDir, args)\n\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\tCache.DeleteDirectory(storage, dstDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\tif !utils.IsBool(lazyCache...) && err == nil && needHandleObjsUpdateHook() {\n\t\tonlyList := false\n\t\ttargetPath := dstDirPath\n\t\tif len(newObjs) == 1 && newObjs[0].IsDir() {\n\t\t\ttargetPath = stdpath.Join(dstDirPath, newObjs[0].GetName())\n\t\t} else if len(newObjs) == 1 && !newObjs[0].IsDir() {\n\t\t\tonlyList = true\n\t\t} else if args.PutIntoNewDir {\n\t\t\ttargetPath = stdpath.Join(dstDirPath, strings.TrimSuffix(srcObj.GetName(), stdpath.Ext(srcObj.GetName())))\n\t\t} else if innerBase := stdpath.Base(args.InnerPath); innerBase != \".\" && innerBase != \"/\" {\n\t\t\ttargetPath = stdpath.Join(dstDirPath, innerBase)\n\t\t\tdstObj, e := Get(ctx, storage, targetPath)\n\t\t\tonlyList = e != nil || !dstObj.IsDir()\n\t\t}\n\t\tif onlyList {\n\t\t\tgo List(context.Background(), storage, dstDirPath, model.ListArgs{Refresh: true})\n\t\t} else {\n\t\t\tvar limiter *rate.Limiter\n\t\t\tif l, _ := GetSettingItemByKey(conf.HandleHookRateLimit); l != nil {\n\t\t\t\tif f, e := strconv.ParseFloat(l.Value, 64); e == nil && f > .0 {\n\t\t\t\t\tlimiter = rate.NewLimiter(rate.Limit(f), 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tgo RecursivelyListStorage(context.Background(), storage, targetPath, limiter, nil)\n\t\t}\n\t}\n\treturn errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/op/cache.go",
    "content": "package op\n\nimport (\n\tstdpath \"path\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/cache\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype CacheManager struct {\n\tdirCache     *cache.KeyedCache[*directoryCache]       // Cache for directory listings\n\tlinkCache    *cache.TypedCache[*objWithLink]          // Cache for file links\n\tuserCache    *cache.KeyedCache[*model.User]           // Cache for user data\n\tsettingCache *cache.KeyedCache[any]                   // Cache for settings\n\tdetailCache  *cache.KeyedCache[*model.StorageDetails] // Cache for storage details\n}\n\nfunc NewCacheManager() *CacheManager {\n\treturn &CacheManager{\n\t\tdirCache:     cache.NewKeyedCache[*directoryCache](time.Minute * 5),\n\t\tlinkCache:    cache.NewTypedCache[*objWithLink](time.Minute * 30),\n\t\tuserCache:    cache.NewKeyedCache[*model.User](time.Hour),\n\t\tsettingCache: cache.NewKeyedCache[any](time.Hour),\n\t\tdetailCache:  cache.NewKeyedCache[*model.StorageDetails](time.Minute * 30),\n\t}\n}\n\n// global instance\nvar Cache = NewCacheManager()\n\nfunc Key(storage driver.Driver, path string) string {\n\treturn utils.GetFullPath(storage.GetStorage().MountPath, path)\n}\n\n// recursively delete directory and its children from dirCache\nfunc (cm *CacheManager) DeleteDirectoryTree(storage driver.Driver, dirPath string) {\n\tif storage.Config().NoCache {\n\t\treturn\n\t}\n\tcm.deleteDirectoryTree(Key(storage, dirPath))\n}\nfunc (cm *CacheManager) deleteDirectoryTree(key string) {\n\tif dirCache, exists := cm.dirCache.Pop(key); exists {\n\t\tfor _, obj := range dirCache.objs {\n\t\t\tif obj.IsDir() {\n\t\t\t\tcm.deleteDirectoryTree(stdpath.Join(key, obj.GetName()))\n\t\t\t} else {\n\t\t\t\tcm.linkCache.DeleteKey(stdpath.Join(key, obj.GetName()))\n\t\t\t}\n\t\t}\n\t}\n}\n\n// remove directory from dirCache\nfunc (cm *CacheManager) DeleteDirectory(storage driver.Driver, dirPath string) {\n\tif storage.Config().NoCache {\n\t\treturn\n\t}\n\tcm.dirCache.Delete(Key(storage, dirPath))\n}\n\n// remove object from dirCache.\n// if it's a directory, remove all its children from dirCache too.\n// if it's a file, remove its link from linkCache.\nfunc (cm *CacheManager) removeDirectoryObject(storage driver.Driver, dirPath string, obj model.Obj) {\n\tkey := Key(storage, dirPath)\n\tif !obj.IsDir() {\n\t\tcm.linkCache.DeleteKey(stdpath.Join(key, obj.GetName()))\n\t}\n\n\tif storage.Config().NoCache {\n\t\treturn\n\t}\n\tif cache, exist := cm.dirCache.Get(key); exist {\n\t\tif obj.IsDir() {\n\t\t\tcm.deleteDirectoryTree(stdpath.Join(key, obj.GetName()))\n\t\t}\n\t\tcache.RemoveObject(obj.GetName())\n\t}\n}\n\n// cache user data\nfunc (cm *CacheManager) SetUser(username string, user *model.User) {\n\tcm.userCache.Set(username, user)\n}\n\n// cached user data\nfunc (cm *CacheManager) GetUser(username string) (*model.User, bool) {\n\treturn cm.userCache.Get(username)\n}\n\n// remove user data from cache\nfunc (cm *CacheManager) DeleteUser(username string) {\n\tcm.userCache.Delete(username)\n}\n\n// caches setting\nfunc (cm *CacheManager) SetSetting(key string, setting *model.SettingItem) {\n\tcm.settingCache.Set(key, setting)\n}\n\n// cached setting\nfunc (cm *CacheManager) GetSetting(key string) (*model.SettingItem, bool) {\n\tif data, exists := cm.settingCache.Get(key); exists {\n\t\tif setting, ok := data.(*model.SettingItem); ok {\n\t\t\treturn setting, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// cache setting groups\nfunc (cm *CacheManager) SetSettingGroup(key string, settings []model.SettingItem) {\n\tcm.settingCache.Set(key, settings)\n}\n\n// cached setting group\nfunc (cm *CacheManager) GetSettingGroup(key string) ([]model.SettingItem, bool) {\n\tif data, exists := cm.settingCache.Get(key); exists {\n\t\tif settings, ok := data.([]model.SettingItem); ok {\n\t\t\treturn settings, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc (cm *CacheManager) SetStorageDetails(storage driver.Driver, details *model.StorageDetails) {\n\tif storage.Config().NoCache {\n\t\treturn\n\t}\n\texpiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)\n\tcm.detailCache.SetWithTTL(utils.GetActualMountPath(storage.GetStorage().MountPath), details, expiration)\n}\n\nfunc (cm *CacheManager) GetStorageDetails(storage driver.Driver) (*model.StorageDetails, bool) {\n\treturn cm.detailCache.Get(utils.GetActualMountPath(storage.GetStorage().MountPath))\n}\n\nfunc (cm *CacheManager) InvalidateStorageDetails(storage driver.Driver) {\n\tcm.detailCache.Delete(utils.GetActualMountPath(storage.GetStorage().MountPath))\n}\n\n// clears all caches\nfunc (cm *CacheManager) ClearAll() {\n\tcm.dirCache.Clear()\n\tcm.linkCache.Clear()\n\tcm.userCache.Clear()\n\tcm.settingCache.Clear()\n\tcm.detailCache.Clear()\n}\n\ntype directoryCache struct {\n\tobjs   []model.Obj\n\tsorted []model.Obj\n\tmu     sync.RWMutex\n\n\tdirtyFlags uint8\n}\n\nconst (\n\tdirtyRemove uint8 = 1 << iota // 对象删除：刷新 sorted 副本，但不需要 full sort/extract\n\tdirtyUpdate                   // 对象更新：需要执行 full sort + extract\n)\n\nfunc newDirectoryCache(objs []model.Obj) *directoryCache {\n\tsorted := make([]model.Obj, len(objs))\n\tcopy(sorted, objs)\n\treturn &directoryCache{\n\t\tobjs:   objs,\n\t\tsorted: sorted,\n\t}\n}\n\nfunc (dc *directoryCache) RemoveObject(name string) {\n\tdc.mu.Lock()\n\tdefer dc.mu.Unlock()\n\tfor i, obj := range dc.objs {\n\t\tif obj.GetName() == name {\n\t\t\tdc.objs = append(dc.objs[:i], dc.objs[i+1:]...)\n\t\t\tdc.dirtyFlags |= dirtyRemove\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (dc *directoryCache) UpdateObject(oldName string, newObj model.Obj) {\n\tdc.mu.Lock()\n\tdefer dc.mu.Unlock()\n\tif oldName != \"\" {\n\t\tfor i, obj := range dc.objs {\n\t\t\tif obj.GetName() == oldName {\n\t\t\t\tdc.objs[i] = newObj\n\t\t\t\tdc.dirtyFlags |= dirtyUpdate\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tdc.objs = append(dc.objs, newObj)\n\tdc.dirtyFlags |= dirtyUpdate\n}\n\nfunc (dc *directoryCache) GetSortedObjects(meta driver.Meta) []model.Obj {\n\tdc.mu.RLock()\n\tif dc.dirtyFlags == 0 {\n\t\tdc.mu.RUnlock()\n\t\treturn dc.sorted\n\t}\n\tdc.mu.RUnlock()\n\tdc.mu.Lock()\n\tdefer dc.mu.Unlock()\n\n\tsorted := make([]model.Obj, len(dc.objs))\n\tcopy(sorted, dc.objs)\n\tdc.sorted = sorted\n\tif dc.dirtyFlags&dirtyUpdate != 0 {\n\t\tstorage := meta.GetStorage()\n\t\tif meta.Config().LocalSort {\n\t\t\tmodel.SortFiles(sorted, storage.OrderBy, storage.OrderDirection)\n\t\t}\n\t\tmodel.ExtractFolder(sorted, storage.ExtractFolder)\n\t}\n\tdc.dirtyFlags = 0\n\treturn sorted\n}\n"
  },
  {
    "path": "internal/op/const.go",
    "content": "package op\n\nconst (\n\tWORK     = \"work\"\n\tDISABLED = \"disabled\"\n\tRootName = \"root\"\n)\n"
  },
  {
    "path": "internal/op/driver.go",
    "content": "package op\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/pkg/errors\"\n)\n\ntype DriverConstructor func() driver.Driver\n\nvar driverMap = map[string]DriverConstructor{}\nvar driverInfoMap = map[string]driver.Info{}\n\nfunc RegisterDriver(driver DriverConstructor) {\n\t// log.Infof(\"register driver: [%s]\", config.Name)\n\ttempDriver := driver()\n\ttempConfig := tempDriver.Config()\n\tregisterDriverItems(tempConfig, tempDriver.GetAddition())\n\tdriverMap[tempConfig.Name] = driver\n}\n\nfunc GetDriver(name string) (DriverConstructor, error) {\n\tn, ok := driverMap[name]\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"no driver named: %s\", name)\n\t}\n\treturn n, nil\n}\n\nfunc GetDriverNames() []string {\n\tvar driverNames []string\n\tfor k := range driverInfoMap {\n\t\tdriverNames = append(driverNames, k)\n\t}\n\treturn driverNames\n}\n\nfunc GetDriverInfoMap() map[string]driver.Info {\n\treturn driverInfoMap\n}\n\nfunc registerDriverItems(config driver.Config, addition driver.Additional) {\n\t// log.Debugf(\"addition of %s: %+v\", config.Name, addition)\n\ttAddition := reflect.TypeOf(addition)\n\tfor tAddition.Kind() == reflect.Pointer {\n\t\ttAddition = tAddition.Elem()\n\t}\n\tmainItems := getMainItems(config)\n\tadditionalItems := getAdditionalItems(tAddition, config.DefaultRoot)\n\tdriverInfoMap[config.Name] = driver.Info{\n\t\tCommon:     mainItems,\n\t\tAdditional: additionalItems,\n\t\tConfig:     config,\n\t}\n}\n\nfunc getMainItems(config driver.Config) []driver.Item {\n\titems := []driver.Item{{\n\t\tName:     \"mount_path\",\n\t\tType:     conf.TypeString,\n\t\tRequired: true,\n\t\tHelp:     \"The path you want to mount to, it is unique and cannot be repeated\",\n\t}, {\n\t\tName: \"order\",\n\t\tType: conf.TypeNumber,\n\t\tHelp: \"use to sort\",\n\t}, {\n\t\tName: \"remark\",\n\t\tType: conf.TypeText,\n\t}}\n\tif !config.NoCache {\n\t\titems = append(items, driver.Item{\n\t\t\tName:     \"cache_expiration\",\n\t\t\tType:     conf.TypeNumber,\n\t\t\tDefault:  \"30\",\n\t\t\tRequired: true,\n\t\t\tHelp:     \"The cache expiration time for this storage\",\n\t\t})\n\t\titems = append(items, driver.Item{\n\t\t\tName:     \"custom_cache_policies\",\n\t\t\tType:     conf.TypeText,\n\t\t\tDefault:  \"\",\n\t\t\tRequired: false,\n\t\t\tHelp:     \"The cache expiration rules for this storage\",\n\t\t})\n\t}\n\tif config.MustProxy() {\n\t\titems = append(items, driver.Item{\n\t\t\tName:     \"webdav_policy\",\n\t\t\tType:     conf.TypeSelect,\n\t\t\tDefault:  \"native_proxy\",\n\t\t\tOptions:  \"use_proxy_url,native_proxy\",\n\t\t\tRequired: true,\n\t\t})\n\t} else {\n\t\tif config.DefaultProxy() {\n\t\t\titems = append(items, []driver.Item{{\n\t\t\t\tName:    \"web_proxy\",\n\t\t\t\tType:    conf.TypeBool,\n\t\t\t\tDefault: \"true\",\n\t\t\t}, {\n\t\t\t\tName:     \"webdav_policy\",\n\t\t\t\tType:     conf.TypeSelect,\n\t\t\t\tOptions:  \"302_redirect,use_proxy_url,native_proxy\",\n\t\t\t\tDefault:  \"native_proxy\",\n\t\t\t\tRequired: true,\n\t\t\t},\n\t\t\t}...)\n\t\t} else {\n\t\t\titems = append(items, []driver.Item{{\n\t\t\t\tName: \"web_proxy\",\n\t\t\t\tType: conf.TypeBool,\n\t\t\t}, {\n\t\t\t\tName:     \"webdav_policy\",\n\t\t\t\tType:     conf.TypeSelect,\n\t\t\t\tOptions:  \"302_redirect,use_proxy_url,native_proxy\",\n\t\t\t\tDefault:  \"302_redirect\",\n\t\t\t\tRequired: true,\n\t\t\t},\n\t\t\t}...)\n\t\t}\n\t\tif config.ProxyRangeOption {\n\t\t\titem := driver.Item{\n\t\t\t\tName: \"proxy_range\",\n\t\t\t\tType: conf.TypeBool,\n\t\t\t\tHelp: \"Need to enable proxy\",\n\t\t\t}\n\t\t\tif config.Name == \"139Yun\" {\n\t\t\t\titem.Default = \"true\"\n\t\t\t}\n\t\t\titems = append(items, item)\n\t\t}\n\t}\n\titems = append(items, driver.Item{\n\t\tName: \"down_proxy_url\",\n\t\tType: conf.TypeText,\n\t})\n\titems = append(items, driver.Item{\n\t\tName:    \"disable_proxy_sign\",\n\t\tType:    conf.TypeBool,\n\t\tDefault: \"false\",\n\t\tHelp:    \"Disable sign for Download proxy URL\",\n\t})\n\tif config.LocalSort {\n\t\titems = append(items, []driver.Item{{\n\t\t\tName:    \"order_by\",\n\t\t\tType:    conf.TypeSelect,\n\t\t\tOptions: \"name,size,modified\",\n\t\t}, {\n\t\t\tName:    \"order_direction\",\n\t\t\tType:    conf.TypeSelect,\n\t\t\tOptions: \"asc,desc\",\n\t\t}}...)\n\t}\n\titems = append(items, driver.Item{\n\t\tName:    \"extract_folder\",\n\t\tType:    conf.TypeSelect,\n\t\tOptions: \"front,back\",\n\t})\n\titems = append(items, driver.Item{\n\t\tName:     \"disable_index\",\n\t\tType:     conf.TypeBool,\n\t\tDefault:  \"false\",\n\t\tRequired: true,\n\t})\n\titems = append(items, driver.Item{\n\t\tName:     \"enable_sign\",\n\t\tType:     conf.TypeBool,\n\t\tDefault:  \"false\",\n\t\tRequired: true,\n\t})\n\treturn items\n}\nfunc getAdditionalItems(t reflect.Type, defaultRoot string) []driver.Item {\n\tvar items []driver.Item\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\t\tif field.Type.Kind() == reflect.Struct {\n\t\t\titems = append(items, getAdditionalItems(field.Type, defaultRoot)...)\n\t\t\tcontinue\n\t\t}\n\t\ttag := field.Tag\n\t\tignore, ok1 := tag.Lookup(\"ignore\")\n\t\tname, ok2 := tag.Lookup(\"json\")\n\t\tif (ok1 && ignore == \"true\") || !ok2 {\n\t\t\tcontinue\n\t\t}\n\t\titem := driver.Item{\n\t\t\tName:     name,\n\t\t\tType:     strings.ToLower(field.Type.Name()),\n\t\t\tDefault:  tag.Get(\"default\"),\n\t\t\tOptions:  tag.Get(\"options\"),\n\t\t\tRequired: tag.Get(\"required\") == \"true\",\n\t\t\tHelp:     tag.Get(\"help\"),\n\t\t}\n\t\tif tag.Get(\"type\") != \"\" {\n\t\t\titem.Type = tag.Get(\"type\")\n\t\t}\n\t\tif item.Name == \"root_folder_id\" || item.Name == \"root_folder_path\" {\n\t\t\tif item.Default == \"\" {\n\t\t\t\titem.Default = defaultRoot\n\t\t\t}\n\t\t\titem.Required = item.Default != \"\"\n\t\t}\n\t\t// set default type to string\n\t\tif item.Type == \"\" {\n\t\t\titem.Type = \"string\"\n\t\t}\n\t\titems = append(items, item)\n\t}\n\treturn items\n}\n"
  },
  {
    "path": "internal/op/driver_test.go",
    "content": "package op_test\n\nimport (\n\t\"testing\"\n\n\t_ \"github.com/OpenListTeam/OpenList/v4/drivers\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\nfunc TestDriverItemsMap(t *testing.T) {\n\titemsMap := op.GetDriverInfoMap()\n\tif len(itemsMap) != 0 {\n\t\tt.Logf(\"driverInfoMap: %v\", itemsMap)\n\t} else {\n\t\tt.Errorf(\"expected driverInfoMap not empty, but got empty\")\n\t}\n}\n"
  },
  {
    "path": "internal/op/fs.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/bmatcuk/doublestar/v4\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/time/rate\"\n)\n\nvar listG singleflight.Group[[]model.Obj]\n\n// List files in storage, not contains virtual file\nfunc List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs) ([]model.Obj, error) {\n\treturn list(ctx, storage, path, args, nil)\n}\n\nfunc list(ctx context.Context, storage driver.Driver, path string, args model.ListArgs, resultValidator func([]model.Obj) error) ([]model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tlog.Debugf(\"op.List %s\", path)\n\tkey := Key(storage, path)\n\tif !args.Refresh {\n\t\tif dirCache, exists := Cache.dirCache.Get(key); exists {\n\t\t\tlog.Debugf(\"use cache when list %s\", path)\n\t\t\tobjs := dirCache.GetSortedObjects(storage)\n\t\t\tif resultValidator != nil {\n\t\t\t\tif err := resultValidator(objs); err == nil {\n\t\t\t\t\treturn objs, nil\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn objs, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tobjs, err, _ := listG.Do(key, func() ([]model.Obj, error) {\n\t\tdir, err := GetUnwrap(ctx, storage, path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed get dir\")\n\t\t}\n\t\tlog.Debugf(\"list dir: %+v\", dir)\n\t\tif !dir.IsDir() {\n\t\t\treturn nil, errors.WithStack(errs.NotFolder)\n\t\t}\n\t\tfiles, err := storage.List(ctx, dir, args)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed to list objs\")\n\t\t}\n\t\t// warp obj name\n\t\twrapObjsName(storage, files)\n\t\t// sort objs\n\t\tif storage.Config().LocalSort {\n\t\t\tmodel.SortFiles(files, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)\n\t\t}\n\t\tmodel.ExtractFolder(files, storage.GetStorage().ExtractFolder)\n\n\t\tif !args.SkipHook {\n\t\t\t// call hooks\n\t\t\tgo func(reqPath string, files []model.Obj) {\n\t\t\t\tHandleObjsUpdateHook(context.WithoutCancel(ctx), reqPath, files)\n\t\t\t}(utils.GetFullPath(storage.GetStorage().MountPath, path), files)\n\t\t}\n\n\t\tif !storage.Config().NoCache {\n\t\t\tif len(files) > 0 {\n\t\t\t\tlog.Debugf(\"set cache: %s => %+v\", key, files)\n\n\t\t\t\tttl := storage.GetStorage().CacheExpiration\n\n\t\t\t\tcustomCachePolicies := storage.GetStorage().CustomCachePolicies\n\t\t\t\tif len(customCachePolicies) > 0 {\n\t\t\t\t\tconfigPolicies := strings.Split(customCachePolicies, \"\\n\")\n\t\t\t\t\tfor _, configPolicy := range configPolicies {\n\t\t\t\t\t\tpattern, ttlstr, ok := strings.Cut(strings.TrimSpace(configPolicy), \":\")\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tlog.Warnf(\"Malformed custom cache policy entry: %s in storage %s for path %s. Expected format: pattern:ttl\", configPolicy, storage.GetStorage().MountPath, path)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif match, err1 := doublestar.Match(pattern, path); err1 != nil {\n\t\t\t\t\t\t\tlog.Warnf(\"Invalid glob pattern in custom cache policy: %s, error: %v\", pattern, err1)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t} else if !match {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif configTtl, err1 := strconv.ParseInt(ttlstr, 10, 64); err1 == nil {\n\t\t\t\t\t\t\tttl = int(configTtl)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tduration := time.Minute * time.Duration(ttl)\n\t\t\t\tCache.dirCache.SetWithTTL(key, newDirectoryCache(files), duration)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"del cache: %s\", key)\n\t\t\t\tCache.deleteDirectoryTree(key)\n\t\t\t}\n\t\t}\n\t\treturn files, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resultValidator != nil {\n\t\tif err := resultValidator(objs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn objs, nil\n}\n\n// Get object from list of files\nfunc Get(ctx context.Context, storage driver.Driver, path string, excludeTempObj ...bool) (model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tlog.Debugf(\"op.Get %s\", path)\n\n\t// is root folder\n\tif path == \"/\" {\n\t\tif getRooter, ok := storage.(driver.GetRooter); ok {\n\t\t\trootObj, err := getRooter.GetRoot(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.WithMessage(err, \"failed get root obj\")\n\t\t\t}\n\t\t\treturn rootObj, nil\n\t\t}\n\t\tswitch r := storage.(type) {\n\t\tcase driver.IRootId:\n\t\t\treturn &model.Object{\n\t\t\t\tID:       r.GetRootId(),\n\t\t\t\tName:     RootName,\n\t\t\t\tModified: storage.GetStorage().Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t\tMask:     model.Locked,\n\t\t\t}, nil\n\t\tcase driver.IRootPath:\n\t\t\treturn &model.Object{\n\t\t\t\tPath:     r.GetRootPath(),\n\t\t\t\tName:     RootName,\n\t\t\t\tModified: storage.GetStorage().Modified,\n\t\t\t\tMask:     model.Locked,\n\t\t\t\tIsFolder: true,\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, errors.New(\"please implement GetRooter or IRootPath or IRootId interface\")\n\t}\n\n\t// try get from cache first\n\tdir, name := stdpath.Split(path)\n\tdirCache, dirCacheExists := Cache.dirCache.Get(Key(storage, dir))\n\trefreshList := false\n\texcludeTemp := utils.IsBool(excludeTempObj...)\n\tif dirCacheExists {\n\t\tfiles := dirCache.GetSortedObjects(storage)\n\t\tfor _, f := range files {\n\t\t\tif f.GetName() == name {\n\t\t\t\tif excludeTemp && model.ObjHasMask(f, model.Temp) {\n\t\t\t\t\trefreshList = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn f, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// get the obj directly without list so that we can reduce the io\n\tif g, ok := storage.(driver.Getter); ok {\n\t\tobj, err := g.Get(ctx, path)\n\t\tif err == nil {\n\t\t\treturn obj, nil\n\t\t}\n\t\tif !errs.IsNotImplementError(err) && !errs.IsNotSupportError(err) {\n\t\t\treturn nil, errors.WithMessage(err, \"failed to get obj\")\n\t\t}\n\t}\n\n\tif !dirCacheExists || refreshList {\n\t\tvar obj model.Obj\n\t\tlist(ctx, storage, dir, model.ListArgs{Refresh: refreshList}, func(objs []model.Obj) error {\n\t\t\tfor _, f := range objs {\n\t\t\t\tif f.GetName() == name {\n\t\t\t\t\tif excludeTemp && model.ObjHasMask(f, model.Temp) {\n\t\t\t\t\t\treturn errs.ObjectNotFound\n\t\t\t\t\t}\n\t\t\t\t\tobj = f\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif obj != nil {\n\t\t\treturn obj, nil\n\t\t}\n\t}\n\tlog.Debugf(\"cant find obj with name: %s\", name)\n\treturn nil, errors.WithStack(errs.ObjectNotFound)\n}\n\nfunc GetUnwrap(ctx context.Context, storage driver.Driver, path string) (model.Obj, error) {\n\tobj, err := Get(ctx, storage, path, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn model.UnwrapObjName(obj), err\n}\n\nvar linkG = singleflight.Group[*objWithLink]{}\n\n// Link get link, if is an url. should have an expiry time\nfunc Link(ctx context.Context, storage driver.Driver, path string, args model.LinkArgs) (*model.Link, model.Obj, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\n\tmode := storage.Config().LinkCacheMode\n\tif mode == -1 {\n\t\tmode = storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(path)\n\t}\n\ttypeKey := args.Type\n\tif mode&driver.LinkCacheIP != 0 {\n\t\ttypeKey += \"/\" + args.IP\n\t}\n\tif mode&driver.LinkCacheUA != 0 {\n\t\ttypeKey += \"/\" + args.Header.Get(\"User-Agent\")\n\t}\n\tkey := Key(storage, path)\n\tif ol, exists := Cache.linkCache.GetType(key, typeKey); exists {\n\t\tif ol.link.Expiration != nil ||\n\t\t\tol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {\n\t\t\treturn ol.link, ol.obj, nil\n\t\t}\n\t}\n\n\tfn := func() (*objWithLink, error) {\n\t\tfile, err := GetUnwrap(ctx, storage, path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed to get file\")\n\t\t}\n\t\tif file.IsDir() {\n\t\t\treturn nil, errors.WithStack(errs.NotFile)\n\t\t}\n\n\t\tlink, err := storage.Link(ctx, file, args)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"failed get link\")\n\t\t}\n\t\tol := &objWithLink{link: link, obj: file}\n\t\tif link.Expiration != nil {\n\t\t\tCache.linkCache.SetTypeWithTTL(key, typeKey, ol, *link.Expiration)\n\t\t} else {\n\t\t\tCache.linkCache.SetTypeWithExpirable(key, typeKey, ol, &link.SyncClosers)\n\t\t}\n\t\treturn ol, nil\n\t}\n\tfor {\n\t\tol, err, _ := linkG.Do(key+\"/\"+typeKey, fn)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tif ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {\n\t\t\treturn ol.link, ol.obj, nil\n\t\t}\n\t}\n}\n\n// Other api\nfunc Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (any, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\to, ok := storage.(driver.Other)\n\tif !ok {\n\t\treturn nil, errs.NotImplement\n\t}\n\tobj, err := GetUnwrap(ctx, storage, args.Path)\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to get obj\")\n\t}\n\treturn o.Other(ctx, model.OtherArgs{\n\t\tObj:    obj,\n\t\tMethod: args.Method,\n\t\tData:   args.Data,\n\t})\n}\n\nvar mkdirG singleflight.Group[any]\n\nfunc MakeDir(ctx context.Context, storage driver.Driver, path string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tkey := Key(storage, path)\n\t_, err, _ := mkdirG.Do(key, func() (any, error) {\n\t\t// check if dir exists\n\t\tf, err := Get(ctx, storage, path)\n\t\tif err == nil {\n\t\t\tif f.IsDir() {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\treturn nil, errors.New(\"file exists\")\n\t\t}\n\t\tif !errs.IsObjectNotFound(err) {\n\t\t\treturn nil, errors.WithMessage(err, \"failed to check if dir exists\")\n\t\t}\n\t\tparentPath, dirName := stdpath.Split(path)\n\t\tif err = MakeDir(ctx, storage, parentPath); err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed to make parent dir [%s]\", parentPath)\n\t\t}\n\t\tparentDir, err := GetUnwrap(ctx, storage, parentPath)\n\t\t// this should not happen\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed to get parent dir [%s]\", parentPath)\n\t\t}\n\t\tif model.ObjHasMask(parentDir, model.NoWrite) {\n\t\t\treturn nil, errors.WithStack(errs.PermissionDenied)\n\t\t}\n\n\t\tvar newObj model.Obj\n\t\tswitch s := storage.(type) {\n\t\tcase driver.MkdirResult:\n\t\t\tnewObj, err = s.MakeDir(ctx, parentDir, dirName)\n\t\tcase driver.Mkdir:\n\t\t\terr = s.MakeDir(ctx, parentDir, dirName)\n\t\tdefault:\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\tif storage.Config().NoCache {\n\t\t\treturn nil, nil\n\t\t}\n\t\tif dirCache, exist := Cache.dirCache.Get(Key(storage, parentPath)); exist {\n\t\t\tif newObj == nil {\n\t\t\t\tt := time.Now()\n\t\t\t\tnewObj = &model.Object{\n\t\t\t\t\tName:     dirName,\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t\tModified: t,\n\t\t\t\t\tCtime:    t,\n\t\t\t\t\tMask:     model.Temp,\n\t\t\t\t}\n\t\t\t}\n\t\t\tdirCache.UpdateObject(\"\", wrapObjName(storage, newObj))\n\t\t}\n\t\treturn nil, nil\n\t})\n\treturn err\n}\n\nfunc Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tif utils.PathEqual(srcPath, \"/\") {\n\t\treturn errors.New(\"move root folder is not allowed\")\n\t}\n\tsrcDirPath := stdpath.Dir(srcPath)\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tif dstDirPath == srcDirPath {\n\t\treturn errors.New(\"move in place\")\n\t}\n\tsrcRawObj, err := Get(ctx, storage, srcPath, true)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\tif model.ObjHasMask(srcRawObj, model.NoMove) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\tsrcObj := model.UnwrapObjName(srcRawObj)\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get dst dir\")\n\t}\n\tif model.ObjHasMask(dstDir, model.NoWrite) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\n\tvar newObj model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.MoveResult:\n\t\tnewObj, err = s.Move(ctx, srcObj, dstDir)\n\tcase driver.Move:\n\t\terr = s.Move(ctx, srcObj, dstDir)\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tsrcKey := Key(storage, srcDirPath)\n\tdstKey := Key(storage, dstDirPath)\n\tif !srcRawObj.IsDir() {\n\t\tCache.linkCache.DeleteKey(stdpath.Join(srcKey, srcRawObj.GetName()))\n\t\tCache.linkCache.DeleteKey(stdpath.Join(dstKey, srcRawObj.GetName()))\n\t}\n\tif !storage.Config().NoCache {\n\t\tif cache, exist := Cache.dirCache.Get(srcKey); exist {\n\t\t\tif srcRawObj.IsDir() {\n\t\t\t\tCache.deleteDirectoryTree(stdpath.Join(srcKey, srcRawObj.GetName()))\n\t\t\t}\n\t\t\tcache.RemoveObject(srcRawObj.GetName())\n\t\t}\n\t\tif cache, exist := Cache.dirCache.Get(dstKey); exist {\n\t\t\tif newObj == nil {\n\t\t\t\tnewObj = &model.ObjWrapMask{Obj: srcRawObj, Mask: model.Temp}\n\t\t\t} else {\n\t\t\t\tnewObj = wrapObjName(storage, newObj)\n\t\t\t}\n\t\t\tcache.UpdateObject(srcRawObj.GetName(), newObj)\n\t\t}\n\t}\n\n\tif ctx.Value(conf.SkipHookKey) != nil || !needHandleObjsUpdateHook() {\n\t\treturn nil\n\t}\n\tif !srcObj.IsDir() {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, dstDirPath, false)\n\t} else {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, stdpath.Join(dstDirPath, srcObj.GetName()), true)\n\t}\n\treturn nil\n}\n\nfunc Rename(ctx context.Context, storage driver.Driver, srcPath, dstName string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tif utils.PathEqual(srcPath, \"/\") {\n\t\treturn errors.New(\"rename root folder is not allowed\")\n\t}\n\tsrcRawObj, err := Get(ctx, storage, srcPath, true)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\tif model.ObjHasMask(srcRawObj, model.NoRename) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\tsrcObj := model.UnwrapObjName(srcRawObj)\n\n\tvar newObj model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.RenameResult:\n\t\tnewObj, err = s.Rename(ctx, srcObj, dstName)\n\tcase driver.Rename:\n\t\terr = s.Rename(ctx, srcObj, dstName)\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tdirKey := Key(storage, stdpath.Dir(srcPath))\n\tif !srcRawObj.IsDir() {\n\t\tCache.linkCache.DeleteKey(stdpath.Join(dirKey, srcRawObj.GetName()))\n\t\tCache.linkCache.DeleteKey(stdpath.Join(dirKey, dstName))\n\t}\n\tif !storage.Config().NoCache {\n\t\tif cache, exist := Cache.dirCache.Get(dirKey); exist {\n\t\t\tif srcRawObj.IsDir() {\n\t\t\t\tCache.deleteDirectoryTree(stdpath.Join(dirKey, srcRawObj.GetName()))\n\t\t\t}\n\t\t\tif newObj == nil {\n\t\t\t\tnewObj = &model.ObjWrapMask{Obj: &model.ObjWrapName{Name: dstName, Obj: srcObj}, Mask: model.Temp}\n\t\t\t}\n\t\t\tnewObj = wrapObjName(storage, newObj)\n\t\t\tcache.UpdateObject(srcRawObj.GetName(), newObj)\n\t\t}\n\t}\n\n\tif ctx.Value(conf.SkipHookKey) != nil || !needHandleObjsUpdateHook() {\n\t\treturn nil\n\t}\n\tdstDirPath := stdpath.Dir(srcPath)\n\tif !srcObj.IsDir() {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, dstDirPath, false)\n\t} else {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, stdpath.Join(dstDirPath, srcObj.GetName()), true)\n\t}\n\treturn nil\n}\n\n// Copy Just copy file[s] in a storage\nfunc Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tif dstDirPath == stdpath.Dir(srcPath) {\n\t\treturn errors.New(\"copy in place\")\n\t}\n\tsrcRawObj, err := Get(ctx, storage, srcPath, true)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\t// if model.ObjHasMask(srcRawObj, model.NoCopy) {\n\t// \treturn errors.WithStack(errs.PermissionDenied)\n\t// }\n\tsrcObj := model.UnwrapObjName(srcRawObj)\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get dst dir\")\n\t}\n\tif model.ObjHasMask(dstDir, model.NoWrite) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\n\tvar newObj model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.CopyResult:\n\t\tnewObj, err = s.Copy(ctx, srcObj, dstDir)\n\tcase driver.Copy:\n\t\terr = s.Copy(ctx, srcObj, dstDir)\n\tdefault:\n\t\terr = errs.NotImplement\n\t}\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tdstKey := Key(storage, dstDirPath)\n\tif !srcRawObj.IsDir() {\n\t\tCache.linkCache.DeleteKey(stdpath.Join(dstKey, srcRawObj.GetName()))\n\t}\n\tif !storage.Config().NoCache {\n\t\tif cache, exist := Cache.dirCache.Get(dstKey); exist {\n\t\t\tif newObj == nil {\n\t\t\t\tnewObj = &model.ObjWrapMask{Obj: srcRawObj, Mask: model.Temp}\n\t\t\t} else {\n\t\t\t\tnewObj = wrapObjName(storage, newObj)\n\t\t\t}\n\t\t\tcache.UpdateObject(srcRawObj.GetName(), newObj)\n\t\t}\n\t}\n\n\tif ctx.Value(conf.SkipHookKey) != nil || !needHandleObjsUpdateHook() {\n\t\treturn nil\n\t}\n\tif !srcObj.IsDir() {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, dstDirPath, false)\n\t} else {\n\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, stdpath.Join(dstDirPath, srcObj.GetName()), true)\n\t}\n\treturn nil\n}\n\nfunc Remove(ctx context.Context, storage driver.Driver, path string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif utils.PathEqual(path, \"/\") {\n\t\treturn errors.New(\"delete root folder is not allowed\")\n\t}\n\trawObj, err := Get(ctx, storage, path, true)\n\tif err != nil {\n\t\t// if object not found, it's ok\n\t\tif errs.IsObjectNotFound(err) {\n\t\t\tlog.Debugf(\"%s have been removed\", path)\n\t\t\treturn nil\n\t\t}\n\t\treturn errors.WithMessage(err, \"failed to get object\")\n\t}\n\tif model.ObjHasMask(rawObj, model.NoRemove) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\tdirPath := stdpath.Dir(path)\n\n\tswitch s := storage.(type) {\n\tcase driver.Remove:\n\t\terr = s.Remove(ctx, model.UnwrapObjName(rawObj))\n\t\tif err == nil {\n\t\t\tCache.removeDirectoryObject(storage, dirPath, rawObj)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc Put(ctx context.Context, storage driver.Driver, dstDirPath string, file model.FileStreamer, up driver.UpdateProgress) error {\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", err)\n\t\t}\n\t}()\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\t// UrlTree PUT\n\tif storage.Config().OnlyIndices {\n\t\tvar link string\n\t\tdstDirPath, link = urlTreeSplitLineFormPath(stdpath.Join(dstDirPath, file.GetName()))\n\t\tfile = &stream.FileStream{Obj: &model.Object{Name: link}, Closers: utils.Closers{file}}\n\t}\n\t// if file exist and size = 0, delete it\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tdstPath := stdpath.Join(dstDirPath, file.GetName())\n\ttempName := file.GetName() + \".openlist_to_delete\"\n\ttempPath := stdpath.Join(dstDirPath, tempName)\n\tfi, err := GetUnwrap(ctx, storage, dstPath)\n\tif err == nil {\n\t\tif fi.GetSize() == 0 {\n\t\t\terr = Remove(ctx, storage, dstPath)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithMessagef(err, \"while uploading, failed remove existing file which size = 0\")\n\t\t\t}\n\t\t} else if storage.Config().NoOverwriteUpload {\n\t\t\t// try to rename old obj\n\t\t\terr = Rename(ctx, storage, dstPath, tempName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tfile.SetExist(fi)\n\t\t}\n\t}\n\terr = MakeDir(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to make dir [%s]\", dstDirPath)\n\t}\n\tparentDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\t// this should not happen\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to get dir [%s]\", dstDirPath)\n\t}\n\tif model.ObjHasMask(parentDir, model.NoWrite) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\t// if up is nil, set a default to prevent panic\n\tif up == nil {\n\t\tup = func(p float64) {}\n\t}\n\n\t// 如果小于0，则通过缓存获取完整大小，可能发生于流式上传\n\tif file.GetSize() < 0 {\n\t\tlog.Warnf(\"file size < 0, try to get full size from cache\")\n\t\tfile.CacheFullAndWriter(nil, nil)\n\t}\n\n\tvar newObj model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.PutResult:\n\t\tnewObj, err = s.Put(ctx, parentDir, file, up)\n\tcase driver.Put:\n\t\terr = s.Put(ctx, parentDir, file, up)\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\tif err == nil {\n\t\tCache.linkCache.DeleteKey(Key(storage, dstPath))\n\t\tif !storage.Config().NoCache {\n\t\t\tif cache, exist := Cache.dirCache.Get(Key(storage, dstDirPath)); exist {\n\t\t\t\tif newObj == nil {\n\t\t\t\t\tnewObj = &model.Object{\n\t\t\t\t\t\tName:     file.GetName(),\n\t\t\t\t\t\tSize:     file.GetSize(),\n\t\t\t\t\t\tModified: file.ModTime(),\n\t\t\t\t\t\tCtime:    file.CreateTime(),\n\t\t\t\t\t\tMask:     model.Temp,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnewObj = wrapObjName(storage, newObj)\n\t\t\t\tcache.UpdateObject(newObj.GetName(), newObj)\n\t\t\t}\n\t\t}\n\n\t\tif ctx.Value(conf.SkipHookKey) == nil && needHandleObjsUpdateHook() {\n\t\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, dstDirPath, false)\n\t\t}\n\t}\n\tlog.Debugf(\"put file [%s] done\", file.GetName())\n\tif storage.Config().NoOverwriteUpload && fi != nil && fi.GetSize() > 0 {\n\t\tif err != nil {\n\t\t\t// upload failed, recover old obj\n\t\t\terr := Rename(ctx, storage, tempPath, file.GetName())\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed recover old obj: %+v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\t// upload success, remove old obj\n\t\t\terr = Remove(ctx, storage, tempPath)\n\t\t}\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url string) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tdstPath := stdpath.Join(dstDirPath, dstName)\n\n\tif _, err := Get(ctx, storage, dstPath); err == nil {\n\t\treturn errors.WithStack(errs.ObjectAlreadyExists)\n\t}\n\terr := MakeDir(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to make dir [%s]\", dstDirPath)\n\t}\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to get dir [%s]\", dstDirPath)\n\t}\n\tif model.ObjHasMask(dstDir, model.NoWrite) {\n\t\treturn errors.WithStack(errs.PermissionDenied)\n\t}\n\tvar newObj model.Obj\n\tswitch s := storage.(type) {\n\tcase driver.PutURLResult:\n\t\tnewObj, err = s.PutURL(ctx, dstDir, dstName, url)\n\tcase driver.PutURL:\n\t\terr = s.PutURL(ctx, dstDir, dstName, url)\n\tdefault:\n\t\treturn errors.WithStack(errs.NotImplement)\n\t}\n\tif err == nil {\n\t\tCache.linkCache.DeleteKey(Key(storage, dstPath))\n\t\tif !storage.Config().NoCache {\n\t\t\tif cache, exist := Cache.dirCache.Get(Key(storage, dstDirPath)); exist {\n\t\t\t\tif newObj == nil {\n\t\t\t\t\tt := time.Now()\n\t\t\t\t\tnewObj = &model.Object{\n\t\t\t\t\t\tName:     dstName,\n\t\t\t\t\t\tModified: t,\n\t\t\t\t\t\tCtime:    t,\n\t\t\t\t\t\tMask:     model.Temp,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnewObj = wrapObjName(storage, newObj)\n\t\t\t\tcache.UpdateObject(newObj.GetName(), newObj)\n\t\t\t}\n\n\t\t\tif ctx.Value(conf.SkipHookKey) == nil && needHandleObjsUpdateHook() {\n\t\t\t\tgo objsUpdateHook(context.WithoutCancel(ctx), storage, dstDirPath, false)\n\t\t\t}\n\t\t}\n\t}\n\tlog.Debugf(\"put url [%s](%s) done\", dstName, url)\n\treturn errors.WithStack(err)\n}\n\nfunc GetDirectUploadTools(storage driver.Driver) []string {\n\tdu, ok := storage.(driver.DirectUploader)\n\tif !ok {\n\t\treturn nil\n\t}\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil\n\t}\n\treturn du.GetDirectUploadTools()\n}\n\nfunc GetDirectUploadInfo(ctx context.Context, tool string, storage driver.Driver, dstDirPath, dstName string, fileSize int64) (any, error) {\n\tdu, ok := storage.(driver.DirectUploader)\n\tif !ok {\n\t\treturn nil, errors.WithStack(errs.NotImplement)\n\t}\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tdstPath := stdpath.Join(dstDirPath, dstName)\n\t_, err := Get(ctx, storage, dstPath)\n\tif err == nil {\n\t\treturn nil, errors.WithStack(errs.ObjectAlreadyExists)\n\t}\n\terr = MakeDir(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to make dir [%s]\", dstDirPath)\n\t}\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn nil, errors.WithMessagef(err, \"failed to get dir [%s]\", dstDirPath)\n\t}\n\tinfo, err := du.GetDirectUploadInfo(ctx, tool, dstDir, dstName, fileSize)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn info, nil\n}\n\nfunc objsUpdateHook(ctx context.Context, storage driver.Driver, dirPath string, recursive bool) {\n\tfiles, err := List(ctx, storage, dirPath, model.ListArgs{SkipHook: true})\n\tif err != nil {\n\t\treturn\n\t}\n\tif !recursive {\n\t\tHandleObjsUpdateHook(ctx, utils.GetFullPath(storage.GetStorage().MountPath, dirPath), files)\n\t\treturn\n\t}\n\tvar limiter *rate.Limiter\n\tif l, _ := GetSettingItemByKey(conf.HandleHookRateLimit); l != nil {\n\t\tif f, e := strconv.ParseFloat(l.Value, 64); e == nil && f > .0 {\n\t\t\tlimiter = rate.NewLimiter(rate.Limit(f), 1)\n\t\t}\n\t}\n\trecursivelyObjsUpdateHook(ctx, storage, dirPath, files, limiter)\n}\nfunc recursivelyObjsUpdateHook(ctx context.Context, storage driver.Driver, dirPath string, files []model.Obj, limiter *rate.Limiter) {\n\tHandleObjsUpdateHook(ctx, utils.GetFullPath(storage.GetStorage().MountPath, dirPath), files)\n\tfor _, f := range files {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn\n\t\t}\n\t\tif !f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tdstPath := stdpath.Join(dirPath, f.GetName())\n\t\tif limiter != nil {\n\t\t\tif err := limiter.Wait(ctx); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tfiles, err := List(ctx, storage, dstPath, model.ListArgs{SkipHook: true})\n\t\tif err == nil {\n\t\t\trecursivelyObjsUpdateHook(ctx, storage, dstPath, files, limiter)\n\t\t}\n\t}\n}\n\nfunc needHandleObjsUpdateHook() bool {\n\tif len(objsUpdateHooks) < 1 {\n\t\treturn false\n\t}\n\tneedHandle, _ := GetSettingItemByKey(conf.HandleHookAfterWriting)\n\treturn needHandle != nil && (needHandle.Value == \"true\" || needHandle.Value == \"1\")\n}\n\nfunc wrapObjsName(storage driver.Driver, objs []model.Obj) {\n\tif _, ok := storage.(driver.Getter); !ok {\n\t\tmodel.WrapObjsName(objs)\n\t}\n}\nfunc wrapObjName(storage driver.Driver, obj model.Obj) model.Obj {\n\tif _, ok := storage.(driver.Getter); !ok {\n\t\treturn model.WrapObjName(obj)\n\t}\n\treturn obj\n}\n"
  },
  {
    "path": "internal/op/hook.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Obj\ntype ObjsUpdateHook = func(ctx context.Context, parent string, objs []model.Obj)\n\nvar (\n\tobjsUpdateHooks = make([]ObjsUpdateHook, 0)\n)\n\nfunc RegisterObjsUpdateHook(hook ObjsUpdateHook) {\n\tobjsUpdateHooks = append(objsUpdateHooks, hook)\n}\n\nfunc HandleObjsUpdateHook(ctx context.Context, parent string, objs []model.Obj) {\n\tfor _, hook := range objsUpdateHooks {\n\t\thook(ctx, parent, objs)\n\t}\n}\n\n// Setting\ntype SettingItemHook func(item *model.SettingItem) error\n\nvar settingItemHooks = map[string]SettingItemHook{\n\tconf.VideoTypes: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.VideoTypes] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.AudioTypes: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.AudioTypes] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.ImageTypes: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.ImageTypes] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.TextTypes: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.TextTypes] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.ProxyTypes: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.ProxyTypes] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.ProxyIgnoreHeaders: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.ProxyIgnoreHeaders] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n\tconf.PrivacyRegs: func(item *model.SettingItem) error {\n\t\tregStrs := strings.Split(item.Value, \"\\n\")\n\t\tregs := make([]*regexp.Regexp, 0, len(regStrs))\n\t\tfor _, regStr := range regStrs {\n\t\t\treg, err := regexp.Compile(regStr)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tregs = append(regs, reg)\n\t\t}\n\t\tconf.PrivacyReg = regs\n\t\treturn nil\n\t},\n\tconf.FilenameCharMapping: func(item *model.SettingItem) error {\n\t\terr := utils.Json.UnmarshalFromString(item.Value, &conf.FilenameCharMap)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debugf(\"filename char mapping: %+v\", conf.FilenameCharMap)\n\t\treturn nil\n\t},\n\tconf.IgnoreDirectLinkParams: func(item *model.SettingItem) error {\n\t\tconf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, \",\")\n\t\treturn nil\n\t},\n}\n\nfunc RegisterSettingItemHook(key string, hook SettingItemHook) {\n\tsettingItemHooks[key] = hook\n}\n\nfunc HandleSettingItemHook(item *model.SettingItem) (hasHook bool, err error) {\n\tif hook, ok := settingItemHooks[item.Key]; ok {\n\t\treturn true, hook(item)\n\t}\n\treturn false, nil\n}\n\n// Storage\ntype StorageHook func(typ string, storage driver.Driver)\n\nvar storageHooks = make([]StorageHook, 0)\n\nfunc callStorageHooks(typ string, storage driver.Driver) {\n\tfor _, hook := range storageHooks {\n\t\thook(typ, storage)\n\t}\n}\n\nfunc RegisterStorageHook(hook StorageHook) {\n\tstorageHooks = append(storageHooks, hook)\n}\n"
  },
  {
    "path": "internal/op/meta.go",
    "content": "package op\n\nimport (\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nvar metaCache = cache.NewMemCache(cache.WithShards[*model.Meta](2))\n\n// metaG maybe not needed\nvar metaG singleflight.Group[*model.Meta]\n\nfunc GetNearestMeta(path string) (*model.Meta, error) {\n\treturn getNearestMeta(utils.FixAndCleanPath(path))\n}\nfunc getNearestMeta(path string) (*model.Meta, error) {\n\tmeta, err := GetMetaByPath(path)\n\tif err == nil {\n\t\treturn meta, nil\n\t}\n\tif errors.Cause(err) != errs.MetaNotFound {\n\t\treturn nil, err\n\t}\n\tif path == \"/\" {\n\t\treturn nil, errs.MetaNotFound\n\t}\n\treturn getNearestMeta(stdpath.Dir(path))\n}\n\nfunc GetMetaByPath(path string) (*model.Meta, error) {\n\treturn getMetaByPath(utils.FixAndCleanPath(path))\n}\nfunc getMetaByPath(path string) (*model.Meta, error) {\n\tmeta, ok := metaCache.Get(path)\n\tif ok {\n\t\tif meta == nil {\n\t\t\treturn meta, errs.MetaNotFound\n\t\t}\n\t\treturn meta, nil\n\t}\n\tmeta, err, _ := metaG.Do(path, func() (*model.Meta, error) {\n\t\t_meta, err := db.GetMetaByPath(path)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\t\tmetaCache.Set(path, nil)\n\t\t\t\treturn nil, errs.MetaNotFound\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tmetaCache.Set(path, _meta, cache.WithEx[*model.Meta](time.Hour))\n\t\treturn _meta, nil\n\t})\n\treturn meta, err\n}\n\nfunc DeleteMetaById(id uint) error {\n\told, err := db.GetMetaById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmetaCache.Del(old.Path)\n\treturn db.DeleteMetaById(id)\n}\n\nfunc UpdateMeta(u *model.Meta) error {\n\tu.Path = utils.FixAndCleanPath(u.Path)\n\told, err := db.GetMetaById(u.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmetaCache.Del(old.Path)\n\treturn db.UpdateMeta(u)\n}\n\nfunc CreateMeta(u *model.Meta) error {\n\tu.Path = utils.FixAndCleanPath(u.Path)\n\tmetaCache.Del(u.Path)\n\treturn db.CreateMeta(u)\n}\n\nfunc GetMetaById(id uint) (*model.Meta, error) {\n\treturn db.GetMetaById(id)\n}\n\nfunc GetMetas(pageIndex, pageSize int) (metas []model.Meta, count int64, err error) {\n\treturn db.GetMetas(pageIndex, pageSize)\n}\n"
  },
  {
    "path": "internal/op/path.go",
    "content": "package op\n\nimport (\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// GetStorageAndActualPath Get the corresponding storage and actual path\n// for path: remove the mount path prefix and join the actual root folder if exists\nfunc GetStorageAndActualPath(rawPath string) (storage driver.Driver, actualPath string, err error) {\n\trawPath = utils.FixAndCleanPath(rawPath)\n\tstorage = GetBalancedStorage(rawPath)\n\tif storage == nil {\n\t\tif rawPath == \"/\" {\n\t\t\terr = errs.NewErr(errs.StorageNotFound, \"please add a storage first\")\n\t\t\treturn\n\t\t}\n\t\terr = errs.NewErr(errs.StorageNotFound, \"rawPath: %s\", rawPath)\n\t\treturn\n\t}\n\tlog.Debugln(\"use storage: \", storage.GetStorage().MountPath)\n\tmountPath := utils.GetActualMountPath(storage.GetStorage().MountPath)\n\tactualPath = utils.FixAndCleanPath(strings.TrimPrefix(rawPath, mountPath))\n\treturn\n}\n\n// urlTreeSplitLineFormPath 分割path中分割真实路径和UrlTree定义字符串\nfunc urlTreeSplitLineFormPath(path string) (pp string, file string) {\n\t// url.PathUnescape 会移除 // ，手动加回去\n\tpath = strings.Replace(path, \"https:/\", \"https://\", 1)\n\tpath = strings.Replace(path, \"http:/\", \"http://\", 1)\n\tif strings.Contains(path, \":https:/\") || strings.Contains(path, \":http:/\") {\n\t\t// URL-Tree模式 /url_tree_drivr/file_name[:size[:time]]:https://example.com/file\n\t\tfPath := strings.SplitN(path, \":\", 2)[0]\n\t\tpp, _ = stdpath.Split(fPath)\n\t\tfile = path[len(pp):]\n\t} else if strings.Contains(path, \"/https:/\") || strings.Contains(path, \"/http:/\") {\n\t\t// URL-Tree模式 /url_tree_drivr/https://example.com/file\n\t\tindex := strings.Index(path, \"/http://\")\n\t\tif index == -1 {\n\t\t\tindex = strings.Index(path, \"/https://\")\n\t\t}\n\t\tpp = path[:index]\n\t\tfile = path[index+1:]\n\t} else {\n\t\tpp, file = stdpath.Split(path)\n\t}\n\tif pp == \"\" {\n\t\tpp = \"/\"\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/op/recursive_list.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/time/rate\"\n)\n\nvar (\n\tManualScanCancel = atomic.Pointer[context.CancelFunc]{}\n\tScannedCount     = atomic.Uint64{}\n)\n\nfunc ManualScanRunning() bool {\n\treturn ManualScanCancel.Load() != nil\n}\n\nfunc BeginManualScan(rawPath string, limit float64) error {\n\trawPath = utils.FixAndCleanPath(rawPath)\n\tctx, cancel := context.WithCancel(context.Background())\n\tif !ManualScanCancel.CompareAndSwap(nil, &cancel) {\n\t\tcancel()\n\t\treturn errors.New(\"manual scan is running, please try later\")\n\t}\n\tScannedCount.Store(0)\n\tgo func() {\n\t\tdefer func() { (*ManualScanCancel.Swap(nil))() }()\n\t\terr := RecursivelyList(ctx, rawPath, rate.Limit(limit), &ScannedCount)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed recursively list: %v\", err)\n\t\t}\n\t}()\n\treturn nil\n}\n\nfunc StopManualScan() {\n\tc := ManualScanCancel.Load()\n\tif c != nil {\n\t\t(*c)()\n\t}\n}\n\nfunc RecursivelyList(ctx context.Context, rawPath string, limit rate.Limit, counter *atomic.Uint64) error {\n\tstorage, actualPath, err := GetStorageAndActualPath(rawPath)\n\tif err != nil && !errors.Is(err, errs.StorageNotFound) {\n\t\treturn err\n\t} else if err == nil {\n\t\tvar limiter *rate.Limiter\n\t\tif limit > .0 {\n\t\t\tlimiter = rate.NewLimiter(limit, 1)\n\t\t}\n\t\tRecursivelyListStorage(ctx, storage, actualPath, limiter, counter)\n\t} else {\n\t\tvar wg sync.WaitGroup\n\t\trecursivelyListVirtual(ctx, rawPath, limit, counter, &wg)\n\t\twg.Wait()\n\t}\n\treturn nil\n}\n\nfunc recursivelyListVirtual(ctx context.Context, rawPath string, limit rate.Limit, counter *atomic.Uint64, wg *sync.WaitGroup) {\n\tobjs := GetStorageVirtualFilesByPath(rawPath)\n\tif counter != nil {\n\t\tcounter.Add(uint64(len(objs)))\n\t}\n\tfor _, obj := range objs {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn\n\t\t}\n\t\tnextPath := stdpath.Join(rawPath, obj.GetName())\n\t\tstorage, actualPath, err := GetStorageAndActualPath(nextPath)\n\t\tif err != nil && !errors.Is(err, errs.StorageNotFound) {\n\t\t\tlog.Errorf(\"error recursively list: failed get storage [%s]: %v\", nextPath, err)\n\t\t} else if err == nil {\n\t\t\tvar limiter *rate.Limiter\n\t\t\tif limit > .0 {\n\t\t\t\tlimiter = rate.NewLimiter(limit, 1)\n\t\t\t}\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tRecursivelyListStorage(ctx, storage, actualPath, limiter, counter)\n\t\t\t}()\n\t\t} else {\n\t\t\trecursivelyListVirtual(ctx, nextPath, limit, counter, wg)\n\t\t}\n\t}\n}\n\nfunc RecursivelyListStorage(ctx context.Context, storage driver.Driver, actualPath string, limiter *rate.Limiter, counter *atomic.Uint64) {\n\tobjs, err := List(ctx, storage, actualPath, model.ListArgs{Refresh: true})\n\tif err != nil {\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tlog.Errorf(\"error recursively list: failed list (%s)[%s]: %v\", storage.GetStorage().MountPath, actualPath, err)\n\t\t}\n\t\treturn\n\t}\n\tif counter != nil {\n\t\tcounter.Add(uint64(len(objs)))\n\t}\n\tfor _, obj := range objs {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn\n\t\t}\n\t\tif !obj.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tif limiter != nil {\n\t\t\tif err = limiter.Wait(ctx); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tnextPath := stdpath.Join(actualPath, obj.GetName())\n\t\tRecursivelyListStorage(ctx, storage, nextPath, limiter, counter)\n\t}\n}\n"
  },
  {
    "path": "internal/op/setting.go",
    "content": "package op\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/pkg/errors\"\n)\n\nvar settingG singleflight.Group[*model.SettingItem]\nvar settingCacheF = func(item *model.SettingItem) {\n\tCache.SetSetting(item.Key, item)\n}\n\nvar settingGroupG singleflight.Group[[]model.SettingItem]\nvar settingGroupCacheF = func(key string, items []model.SettingItem) {\n\tCache.SetSettingGroup(key, items)\n}\n\nvar settingChangingCallbacks = make([]func(), 0)\n\nfunc RegisterSettingChangingCallback(f func()) {\n\tsettingChangingCallbacks = append(settingChangingCallbacks, f)\n}\n\nfunc SettingCacheUpdate() {\n\tCache.ClearAll()\n\tfor _, cb := range settingChangingCallbacks {\n\t\tcb()\n\t}\n}\n\nfunc GetPublicSettingsMap() map[string]string {\n\titems, _ := GetPublicSettingItems()\n\tpSettings := make(map[string]string)\n\tfor _, item := range items {\n\t\tpSettings[item.Key] = item.Value\n\t}\n\treturn pSettings\n}\n\nfunc GetSettingsMap() map[string]string {\n\titems, _ := GetSettingItems()\n\tsettings := make(map[string]string)\n\tfor _, item := range items {\n\t\tsettings[item.Key] = item.Value\n\t}\n\treturn settings\n}\n\nfunc GetSettingItems() ([]model.SettingItem, error) {\n\tif items, exists := Cache.GetSettingGroup(\"ALL_SETTING_ITEMS\"); exists {\n\t\treturn items, nil\n\t}\n\titems, err, _ := settingGroupG.Do(\"ALL_SETTING_ITEMS\", func() ([]model.SettingItem, error) {\n\t\t_items, err := db.GetSettingItems()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsettingGroupCacheF(\"ALL_SETTING_ITEMS\", _items)\n\t\treturn _items, nil\n\t})\n\treturn items, err\n}\n\nfunc GetPublicSettingItems() ([]model.SettingItem, error) {\n\tif items, exists := Cache.GetSettingGroup(\"ALL_PUBLIC_SETTING_ITEMS\"); exists {\n\t\treturn items, nil\n\t}\n\titems, err, _ := settingGroupG.Do(\"ALL_PUBLIC_SETTING_ITEMS\", func() ([]model.SettingItem, error) {\n\t\t_items, err := db.GetPublicSettingItems()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsettingGroupCacheF(\"ALL_PUBLIC_SETTING_ITEMS\", _items)\n\t\treturn _items, nil\n\t})\n\treturn items, err\n}\n\nfunc GetSettingItemByKey(key string) (*model.SettingItem, error) {\n\tif item, exists := Cache.GetSetting(key); exists {\n\t\treturn item, nil\n\t}\n\n\titem, err, _ := settingG.Do(key, func() (*model.SettingItem, error) {\n\t\t_item, err := db.GetSettingItemByKey(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsettingCacheF(_item)\n\t\treturn _item, nil\n\t})\n\treturn item, err\n}\n\nfunc GetSettingItemInKeys(keys []string) ([]model.SettingItem, error) {\n\tvar items []model.SettingItem\n\tfor _, key := range keys {\n\t\titem, err := GetSettingItemByKey(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems = append(items, *item)\n\t}\n\treturn items, nil\n}\n\nfunc GetSettingItemsByGroup(group int) ([]model.SettingItem, error) {\n\tkey := fmt.Sprintf(\"GROUP_%d\", group)\n\tif items, exists := Cache.GetSettingGroup(key); exists {\n\t\treturn items, nil\n\t}\n\titems, err, _ := settingGroupG.Do(key, func() ([]model.SettingItem, error) {\n\t\t_items, err := db.GetSettingItemsByGroup(group)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsettingGroupCacheF(key, _items)\n\t\treturn _items, nil\n\t})\n\treturn items, err\n}\n\nfunc GetSettingItemsInGroups(groups []int) ([]model.SettingItem, error) {\n\tsort.Ints(groups)\n\n\tkeyParts := make([]string, 0, len(groups))\n\tfor _, g := range groups {\n\t\tkeyParts = append(keyParts, strconv.Itoa(g))\n\t}\n\tkey := \"GROUPS_\" + strings.Join(keyParts, \"_\")\n\n\tif items, exists := Cache.GetSettingGroup(key); exists {\n\t\treturn items, nil\n\t}\n\titems, err, _ := settingGroupG.Do(key, func() ([]model.SettingItem, error) {\n\t\t_items, err := db.GetSettingItemsInGroups(groups)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsettingGroupCacheF(key, _items)\n\t\treturn _items, nil\n\t})\n\treturn items, err\n}\n\nfunc SaveSettingItems(items []model.SettingItem) error {\n\tfor i := range items {\n\t\titem := &items[i]\n\t\tif it, ok := MigrationSettingItems[item.Key]; ok &&\n\t\t\titem.Value == it.MigrationValue {\n\t\t\titem.Value = it.Value\n\t\t}\n\t\tif ok, err := HandleSettingItemHook(item); ok && err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute hook on %s: %+v\", item.Key, err)\n\t\t}\n\t}\n\terr := db.SaveSettingItems(items)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed save setting: %+v\", err)\n\t}\n\tSettingCacheUpdate()\n\treturn nil\n}\n\nfunc SaveSettingItem(item *model.SettingItem) (err error) {\n\tif it, ok := MigrationSettingItems[item.Key]; ok &&\n\t\titem.Value == it.MigrationValue {\n\t\titem.Value = it.Value\n\t}\n\t// hook\n\tif _, err := HandleSettingItemHook(item); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute hook on %s: %+v\", item.Key, err)\n\t}\n\t// update\n\tif err = db.SaveSettingItem(item); err != nil {\n\t\treturn fmt.Errorf(\"failed save setting on %s: %+v\", item.Key, err)\n\t}\n\tSettingCacheUpdate()\n\treturn nil\n}\n\nfunc DeleteSettingItemByKey(key string) error {\n\told, err := GetSettingItemByKey(key)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get settingItem\")\n\t}\n\tif !old.IsDeprecated() {\n\t\treturn errors.Errorf(\"setting [%s] is not deprecated\", key)\n\t}\n\tSettingCacheUpdate()\n\treturn db.DeleteSettingItemByKey(key)\n}\n\ntype MigrationValueItem struct {\n\tMigrationValue, Value string\n}\n\nvar MigrationSettingItems map[string]MigrationValueItem\n"
  },
  {
    "path": "internal/op/sharing.go",
    "content": "package op\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc makeJoined(sdb []model.SharingDB) []model.Sharing {\n\tcreator := make(map[uint]*model.User)\n\treturn utils.MustSliceConvert(sdb, func(s model.SharingDB) model.Sharing {\n\t\tvar c *model.User\n\t\tvar ok bool\n\t\tif c, ok = creator[s.CreatorId]; !ok {\n\t\t\tvar err error\n\t\t\tif c, err = GetUserById(s.CreatorId); err != nil {\n\t\t\t\tc = nil\n\t\t\t} else {\n\t\t\t\tcreator[s.CreatorId] = c\n\t\t\t}\n\t\t}\n\t\tvar files []string\n\t\tif err := utils.Json.UnmarshalFromString(s.FilesRaw, &files); err != nil {\n\t\t\tfiles = make([]string, 0)\n\t\t}\n\t\treturn model.Sharing{\n\t\t\tSharingDB: &s,\n\t\t\tFiles:     files,\n\t\t\tCreator:   c,\n\t\t}\n\t})\n}\n\nvar sharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](8))\nvar sharingG singleflight.Group[*model.Sharing]\n\nfunc GetSharingById(id string, refresh ...bool) (*model.Sharing, error) {\n\tif !utils.IsBool(refresh...) {\n\t\tif sharing, ok := sharingCache.Get(id); ok {\n\t\t\tlog.Debugf(\"use cache when get sharing %s\", id)\n\t\t\treturn sharing, nil\n\t\t}\n\t}\n\tsharing, err, _ := sharingG.Do(id, func() (*model.Sharing, error) {\n\t\ts, err := db.GetSharingById(id)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed get sharing [%s]\", id)\n\t\t}\n\t\tcreator, err := GetUserById(s.CreatorId)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed get sharing creator [%s]\", id)\n\t\t}\n\t\tvar files []string\n\t\tif err = utils.Json.UnmarshalFromString(s.FilesRaw, &files); err != nil {\n\t\t\tfiles = make([]string, 0)\n\t\t}\n\t\treturn &model.Sharing{\n\t\t\tSharingDB: s,\n\t\t\tFiles:     files,\n\t\t\tCreator:   creator,\n\t\t}, nil\n\t})\n\treturn sharing, err\n}\n\nfunc GetSharings(pageIndex, pageSize int) ([]model.Sharing, int64, error) {\n\ts, cnt, err := db.GetSharings(pageIndex, pageSize)\n\tif err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\treturn makeJoined(s), cnt, nil\n}\n\nfunc GetSharingsByCreatorId(userId uint, pageIndex, pageSize int) ([]model.Sharing, int64, error) {\n\ts, cnt, err := db.GetSharingsByCreatorId(userId, pageIndex, pageSize)\n\tif err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\treturn makeJoined(s), cnt, nil\n}\n\nfunc GetSharingUnwrapPath(sharing *model.Sharing, path string) (unwrapPath string, err error) {\n\tif len(sharing.Files) == 0 {\n\t\treturn \"\", errors.New(\"cannot get actual path of an invalid sharing\")\n\t}\n\tif len(sharing.Files) == 1 {\n\t\treturn stdpath.Join(sharing.Files[0], path), nil\n\t}\n\tpath = utils.FixAndCleanPath(path)[1:]\n\tif len(path) == 0 {\n\t\treturn \"\", errors.New(\"cannot get actual path of a sharing root path\")\n\t}\n\tmapPath := \"\"\n\tchild, rest, _ := strings.Cut(path, \"/\")\n\tfor _, c := range sharing.Files {\n\t\tif child == stdpath.Base(c) {\n\t\t\tmapPath = c\n\t\t\tbreak\n\t\t}\n\t}\n\tif mapPath == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed find child [%s] of sharing [%s]\", child, sharing.ID)\n\t}\n\treturn stdpath.Join(mapPath, rest), nil\n}\n\nfunc CreateSharing(sharing *model.Sharing) (id string, err error) {\n\tsharing.CreatorId = sharing.Creator.ID\n\tsharing.FilesRaw, err = utils.Json.MarshalToString(utils.MustSliceConvert(sharing.Files, utils.FixAndCleanPath))\n\tif err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\treturn db.CreateSharing(sharing.SharingDB)\n}\n\nfunc UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {\n\tif !utils.IsBool(skipMarshal...) {\n\t\tsharing.CreatorId = sharing.Creator.ID\n\t\tsharing.FilesRaw, err = utils.Json.MarshalToString(utils.MustSliceConvert(sharing.Files, utils.FixAndCleanPath))\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\tsharingCache.Del(sharing.ID)\n\treturn db.UpdateSharing(sharing.SharingDB)\n}\n\nfunc DeleteSharing(sid string) error {\n\tsharingCache.Del(sid)\n\treturn db.DeleteSharingById(sid)\n}\n\nfunc DeleteSharingsByCreatorId(creatorId uint) error {\n\treturn db.DeleteSharingsByCreatorId(creatorId)\n}\n"
  },
  {
    "path": "internal/op/sshkey.go",
    "content": "package op\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc CreateSSHPublicKey(k *model.SSHPublicKey) (error, bool) {\n\t_, err := db.GetSSHPublicKeyByUserTitle(k.UserId, k.Title)\n\tif err == nil {\n\t\treturn errors.New(\"key with the same title already exists\"), true\n\t}\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.KeyStr))\n\tif err != nil {\n\t\treturn err, false\n\t}\n\tk.Fingerprint = ssh.FingerprintSHA256(pubKey)\n\tk.AddedTime = time.Now()\n\tk.LastUsedTime = k.AddedTime\n\treturn db.CreateSSHPublicKey(k), true\n}\n\nfunc GetSSHPublicKeyByUserId(userId uint, pageIndex, pageSize int) (keys []model.SSHPublicKey, count int64, err error) {\n\treturn db.GetSSHPublicKeyByUserId(userId, pageIndex, pageSize)\n}\n\nfunc GetSSHPublicKeyByIdAndUserId(id uint, userId uint) (*model.SSHPublicKey, error) {\n\tkey, err := db.GetSSHPublicKeyById(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif key.UserId != userId {\n\t\treturn nil, errors.Wrapf(err, \"failed get old key\")\n\t}\n\treturn key, nil\n}\n\nfunc UpdateSSHPublicKey(k *model.SSHPublicKey) error {\n\treturn db.UpdateSSHPublicKey(k)\n}\n\nfunc DeleteSSHPublicKeyById(keyId uint) error {\n\treturn db.DeleteSSHPublicKeyById(keyId)\n}\n"
  },
  {
    "path": "internal/op/storage.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Although the driver type is stored,\n// there is a storage in each driver,\n// so it should actually be a storage, just wrapped by the driver\nvar storagesMap generic_sync.MapOf[string, driver.Driver]\n\nfunc GetAllStorages() []driver.Driver {\n\treturn storagesMap.Values()\n}\n\nfunc HasStorage(mountPath string) bool {\n\treturn storagesMap.Has(utils.FixAndCleanPath(mountPath))\n}\n\nfunc GetStorageByMountPath(mountPath string) (driver.Driver, error) {\n\tmountPath = utils.FixAndCleanPath(mountPath)\n\tstorageDriver, ok := storagesMap.Load(mountPath)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"no mount path for an storage is: %s\", mountPath)\n\t}\n\treturn storageDriver, nil\n}\n\n// CreateStorage Save the storage to database so storage can get an id\n// then instantiate corresponding driver and save it in memory\nfunc CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {\n\tstorage.Modified = time.Now()\n\tstorage.MountPath = utils.FixAndCleanPath(storage.MountPath)\n\tvar err error\n\t// check driver first\n\tdriverName := storage.Driver\n\tdriverNew, err := GetDriver(driverName)\n\tif err != nil {\n\t\treturn 0, errors.WithMessage(err, \"failed get driver new\")\n\t}\n\tstorageDriver := driverNew()\n\t// insert storage to database\n\terr = db.CreateStorage(&storage)\n\tif err != nil {\n\t\treturn storage.ID, errors.WithMessage(err, \"failed create storage in database\")\n\t}\n\t// already has an id\n\terr = initStorage(ctx, storage, storageDriver)\n\tgo callStorageHooks(\"add\", storageDriver)\n\tif err != nil {\n\t\treturn storage.ID, errors.Wrap(err, \"failed init storage but storage is already created\")\n\t}\n\tlog.Debugf(\"storage %+v is created\", storageDriver)\n\treturn storage.ID, nil\n}\n\n// LoadStorage load exist storage in db to memory\nfunc LoadStorage(ctx context.Context, storage model.Storage) error {\n\tstorage.MountPath = utils.FixAndCleanPath(storage.MountPath)\n\t// check driver first\n\tdriverName := storage.Driver\n\tdriverNew, err := GetDriver(driverName)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get driver new\")\n\t}\n\tstorageDriver := driverNew()\n\n\terr = initStorage(ctx, storage, storageDriver)\n\tgo callStorageHooks(\"add\", storageDriver)\n\tlog.Debugf(\"storage %+v is created\", storageDriver)\n\treturn err\n}\n\nfunc getCurrentGoroutineStack() string {\n\tbuf := make([]byte, 1<<16)\n\tn := runtime.Stack(buf, false)\n\treturn string(buf[:n])\n}\n\n// initStorage initialize the driver and store to storagesMap\nfunc initStorage(ctx context.Context, storage model.Storage, storageDriver driver.Driver) (err error) {\n\tstorageDriver.SetStorage(storage)\n\tdriverStorage := storageDriver.GetStorage()\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\terrInfo := fmt.Sprintf(\"[panic] err: %v\\nstack: %s\\n\", err, getCurrentGoroutineStack())\n\t\t\tlog.Errorf(\"panic init storage: %s\", errInfo)\n\t\t\tdriverStorage.SetStatus(errInfo)\n\t\t\tMustSaveDriverStorage(storageDriver)\n\t\t\tstoragesMap.Store(driverStorage.MountPath, storageDriver)\n\t\t}\n\t}()\n\t// Unmarshal Addition\n\terr = utils.Json.UnmarshalFromString(driverStorage.Addition, storageDriver.GetAddition())\n\tif err == nil {\n\t\tif ref, ok := storageDriver.(driver.Reference); ok {\n\t\t\tif strings.HasPrefix(driverStorage.Remark, \"ref:/\") {\n\t\t\t\trefMountPath := driverStorage.Remark\n\t\t\t\ti := strings.Index(refMountPath, \"\\n\")\n\t\t\t\tif i > 0 {\n\t\t\t\t\trefMountPath = refMountPath[4:i]\n\t\t\t\t} else {\n\t\t\t\t\trefMountPath = refMountPath[4:]\n\t\t\t\t}\n\t\t\t\tvar refStorage driver.Driver\n\t\t\t\trefStorage, err = GetStorageByMountPath(refMountPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = fmt.Errorf(\"ref: %w\", err)\n\t\t\t\t} else {\n\t\t\t\t\terr = ref.InitReference(refStorage)\n\t\t\t\t\tif err != nil && errs.IsNotSupportError(err) {\n\t\t\t\t\t\terr = fmt.Errorf(\"ref: storage is not %s\", storageDriver.Config().Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif err == nil {\n\t\terr = storageDriver.Init(ctx)\n\t}\n\tstoragesMap.Store(driverStorage.MountPath, storageDriver)\n\tif err != nil {\n\t\tif IsUseOnlineAPI(storageDriver) {\n\t\t\tdriverStorage.SetStatus(utils.SanitizeHTML(err.Error()))\n\t\t} else {\n\t\t\tdriverStorage.SetStatus(err.Error())\n\t\t}\n\t\terr = errors.Wrap(err, \"failed init storage\")\n\t} else {\n\t\tdriverStorage.SetStatus(WORK)\n\t}\n\tMustSaveDriverStorage(storageDriver)\n\treturn err\n}\n\nfunc IsUseOnlineAPI(storageDriver driver.Driver) bool {\n\tv := reflect.ValueOf(storageDriver.GetAddition())\n\tif v.Kind() == reflect.Ptr {\n\t\tv = v.Elem()\n\t}\n\tif !v.IsValid() || v.Kind() != reflect.Struct {\n\t\treturn false\n\t}\n\tfield_v := v.FieldByName(\"UseOnlineAPI\")\n\tif !field_v.IsValid() {\n\t\treturn false\n\t}\n\tif field_v.Kind() != reflect.Bool {\n\t\treturn false\n\t}\n\treturn field_v.Bool()\n}\n\nfunc EnableStorage(ctx context.Context, id uint) error {\n\tstorage, err := db.GetStorageById(id)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif !storage.Disabled {\n\t\treturn errors.Errorf(\"this storage have enabled\")\n\t}\n\tstorage.Disabled = false\n\terr = db.UpdateStorage(storage)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed update storage in db\")\n\t}\n\terr = LoadStorage(ctx, *storage)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed load storage\")\n\t}\n\treturn nil\n}\n\nfunc DisableStorage(ctx context.Context, id uint) error {\n\tstorage, err := db.GetStorageById(id)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tif storage.Disabled {\n\t\treturn errors.Errorf(\"this storage have disabled\")\n\t}\n\tstorageDriver, err := GetStorageByMountPath(storage.MountPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage driver\")\n\t}\n\t// drop the storage in the driver\n\tif err := storageDriver.Drop(ctx); err != nil {\n\t\treturn errors.Wrap(err, \"failed drop storage\")\n\t}\n\t// delete the storage in the memory\n\tstorage.Disabled = true\n\tstorage.SetStatus(DISABLED)\n\terr = db.UpdateStorage(storage)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed update storage in db\")\n\t}\n\tstoragesMap.Delete(storage.MountPath)\n\tgo callStorageHooks(\"del\", storageDriver)\n\treturn nil\n}\n\n// UpdateStorage update storage\n// get old storage first\n// drop the storage then reinitialize\nfunc UpdateStorage(ctx context.Context, storage model.Storage) error {\n\toldStorage, err := db.GetStorageById(storage.ID)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get old storage\")\n\t}\n\tif oldStorage.Driver != storage.Driver {\n\t\treturn errors.Errorf(\"driver cannot be changed\")\n\t}\n\tstorage.Modified = time.Now()\n\tstorage.MountPath = utils.FixAndCleanPath(storage.MountPath)\n\terr = db.UpdateStorage(&storage)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed update storage in database\")\n\t}\n\tif storage.Disabled {\n\t\treturn nil\n\t}\n\tstorageDriver, err := GetStorageByMountPath(oldStorage.MountPath)\n\tif oldStorage.MountPath != storage.MountPath {\n\t\t// mount path renamed, need to drop the storage\n\t\tstoragesMap.Delete(oldStorage.MountPath)\n\t\tCache.DeleteDirectoryTree(storageDriver, \"/\")\n\t\tCache.InvalidateStorageDetails(storageDriver)\n\t}\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage driver\")\n\t}\n\terr = storageDriver.Drop(ctx)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed drop storage\")\n\t}\n\n\terr = initStorage(ctx, storage, storageDriver)\n\tgo callStorageHooks(\"update\", storageDriver)\n\tlog.Debugf(\"storage %+v is update\", storageDriver)\n\treturn err\n}\n\nfunc DeleteStorageById(ctx context.Context, id uint) error {\n\tstorage, err := db.GetStorageById(id)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\tvar dropErr error = nil\n\tif !storage.Disabled {\n\t\tstorageDriver, err := GetStorageByMountPath(storage.MountPath)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed get storage driver\")\n\t\t}\n\t\t// drop the storage in the driver\n\t\tif err := storageDriver.Drop(ctx); err != nil {\n\t\t\tdropErr = errors.Wrapf(err, \"failed drop storage\")\n\t\t}\n\t\t// delete the storage in the memory\n\t\tstoragesMap.Delete(storage.MountPath)\n\t\tCache.DeleteDirectoryTree(storageDriver, \"/\")\n\t\tCache.InvalidateStorageDetails(storageDriver)\n\t\tgo callStorageHooks(\"del\", storageDriver)\n\t}\n\t// delete the storage in the database\n\tif err := db.DeleteStorageById(id); err != nil {\n\t\treturn errors.WithMessage(err, \"failed delete storage in database\")\n\t}\n\treturn dropErr\n}\n\n// MustSaveDriverStorage call from specific driver\nfunc MustSaveDriverStorage(driver driver.Driver) {\n\terr := saveDriverStorage(driver)\n\tif err != nil {\n\t\tlog.Errorf(\"failed save driver storage: %s\", err)\n\t}\n}\n\nfunc saveDriverStorage(driver driver.Driver) error {\n\tstorage := driver.GetStorage()\n\taddition := driver.GetAddition()\n\tstr, err := utils.Json.MarshalToString(addition)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error while marshal addition\")\n\t}\n\tstorage.Addition = str\n\terr = db.UpdateStorage(storage)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed update storage in database\")\n\t}\n\treturn nil\n}\n\n// getStoragesByPath get storage by longest match path, contains balance storage.\n// for example, there is /a/b,/a/c,/a/d/e,/a/d/e.balance\n// getStoragesByPath(/a/d/e/f) => /a/d/e,/a/d/e.balance\nfunc getStoragesByPath(path string) []driver.Driver {\n\tstorages := make([]driver.Driver, 0)\n\tcurSlashCount := 0\n\tstoragesMap.Range(func(mountPath string, value driver.Driver) bool {\n\t\tmountPath = utils.GetActualMountPath(mountPath)\n\t\t// is this path\n\t\tif utils.IsSubPath(mountPath, path) {\n\t\t\tslashCount := strings.Count(utils.PathAddSeparatorSuffix(mountPath), \"/\")\n\t\t\t// not the longest match\n\t\t\tif slashCount > curSlashCount {\n\t\t\t\tstorages = storages[:0]\n\t\t\t\tcurSlashCount = slashCount\n\t\t\t}\n\t\t\tif slashCount == curSlashCount {\n\t\t\t\tstorages = append(storages, value)\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n\t// make sure the order is the same for same input\n\tsort.Slice(storages, func(i, j int) bool {\n\t\treturn storages[i].GetStorage().MountPath < storages[j].GetStorage().MountPath\n\t})\n\treturn storages\n}\n\n// GetStorageVirtualFilesByPath Obtain the virtual file generated by the storage according to the path\n// for example, there are: /a/b,/a/c,/a/d/e,/a/b.balance1,/av\n// GetStorageVirtualFilesByPath(/a) => b,c,d\nfunc GetStorageVirtualFilesByPath(prefix string) []model.Obj {\n\treturn getStorageVirtualFilesByPath(prefix, nil, \"\")\n}\n\nfunc GetStorageVirtualFilesWithDetailsByPath(ctx context.Context, prefix string, hideDetails, refresh bool, filterByName string) []model.Obj {\n\tif hideDetails {\n\t\treturn getStorageVirtualFilesByPath(prefix, nil, filterByName)\n\t}\n\treturn getStorageVirtualFilesByPath(prefix, func(d driver.Driver, obj model.Obj) model.Obj {\n\t\tif _, ok := obj.(*model.ObjStorageDetails); ok {\n\t\t\treturn obj\n\t\t}\n\t\tret := &model.ObjStorageDetails{\n\t\t\tObj:            obj,\n\t\t\tStorageDetails: nil,\n\t\t}\n\t\tresultChan := make(chan *model.StorageDetails, 1)\n\t\tgo func(dri driver.Driver) {\n\t\t\tdetails, err := GetStorageDetails(ctx, dri, refresh)\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {\n\t\t\t\t\tlog.Errorf(\"failed get %s storage details: %+v\", dri.GetStorage().MountPath, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tresultChan <- details\n\t\t}(d)\n\t\tselect {\n\t\tcase r := <-resultChan:\n\t\t\tret.StorageDetails = r\n\t\tcase <-time.After(time.Second):\n\t\t}\n\t\treturn ret\n\t}, filterByName)\n}\n\nfunc getStorageVirtualFilesByPath(prefix string, rootCallback func(driver.Driver, model.Obj) model.Obj, filterByName string) []model.Obj {\n\tfiles := make([]model.Obj, 0)\n\tstorages := storagesMap.Values()\n\tsort.Slice(storages, func(i, j int) bool {\n\t\tif storages[i].GetStorage().Order == storages[j].GetStorage().Order {\n\t\t\treturn storages[i].GetStorage().MountPath < storages[j].GetStorage().MountPath\n\t\t}\n\t\treturn storages[i].GetStorage().Order < storages[j].GetStorage().Order\n\t})\n\n\tif !strings.HasSuffix(prefix, \"/\") {\n\t\tprefix += \"/\"\n\t}\n\tset := make(map[string]int)\n\tvar wg sync.WaitGroup\n\tfor _, v := range storages {\n\t\t// Exclude prefix itself and non prefix\n\t\tp, found := strings.CutPrefix(utils.GetActualMountPath(v.GetStorage().MountPath), prefix)\n\t\tif !found || p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tname, _, found := strings.Cut(p, \"/\")\n\t\tif filterByName != \"\" && name != filterByName {\n\t\t\tcontinue\n\t\t}\n\n\t\tif idx, ok := set[name]; ok {\n\t\t\tif !found {\n\t\t\t\tfiles[idx].(*model.Object).Mask = model.Locked | model.Virtual\n\t\t\t\tif rootCallback != nil {\n\t\t\t\t\twg.Add(1)\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\t\tfiles[idx] = rootCallback(v, files[idx])\n\t\t\t\t\t}()\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tset[name] = len(files)\n\t\tobj := &model.Object{\n\t\t\tName:     name,\n\t\t\tModified: v.GetStorage().Modified,\n\t\t\tIsFolder: true,\n\t\t}\n\t\tif !found {\n\t\t\tidx := len(files)\n\t\t\tobj.Mask = model.Locked | model.Virtual\n\t\t\tfiles = append(files, obj)\n\t\t\tif rootCallback != nil {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tfiles[idx] = rootCallback(v, files[idx])\n\t\t\t\t}()\n\t\t\t}\n\t\t} else {\n\t\t\tobj.Mask = model.ReadOnly | model.Virtual\n\t\t\tfiles = append(files, obj)\n\t\t}\n\t}\n\tif rootCallback != nil {\n\t\twg.Wait()\n\t}\n\treturn files\n}\n\nvar balanceMap generic_sync.MapOf[string, int]\n\n// GetBalancedStorage get storage by path\nfunc GetBalancedStorage(path string) driver.Driver {\n\tpath = utils.FixAndCleanPath(path)\n\tstorages := getStoragesByPath(path)\n\tstorageNum := len(storages)\n\tswitch storageNum {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn storages[0]\n\tdefault:\n\t\tvirtualPath := utils.GetActualMountPath(storages[0].GetStorage().MountPath)\n\t\ti, _ := balanceMap.LoadOrStore(virtualPath, 0)\n\t\ti = (i + 1) % storageNum\n\t\tbalanceMap.Store(virtualPath, i)\n\t\treturn storages[i]\n\t}\n}\n\nvar detailsG singleflight.Group[*model.StorageDetails]\n\nfunc GetStorageDetails(ctx context.Context, storage driver.Driver, refresh ...bool) (*model.StorageDetails, error) {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.WithMessagef(errs.StorageNotInit, \"storage status: %s\", storage.GetStorage().Status)\n\t}\n\twd, ok := storage.(driver.WithDetails)\n\tif !ok {\n\t\treturn nil, errs.NotImplement\n\t}\n\tif !utils.IsBool(refresh...) {\n\t\tif ret, ok := Cache.GetStorageDetails(storage); ok {\n\t\t\treturn ret, nil\n\t\t}\n\t}\n\tdetails, err, _ := detailsG.Do(storage.GetStorage().MountPath, func() (*model.StorageDetails, error) {\n\t\tret, err := wd.GetDetails(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tCache.SetStorageDetails(storage, ret)\n\t\treturn ret, nil\n\t})\n\treturn details, err\n}\n"
  },
  {
    "path": "internal/op/storage_test.go",
    "content": "package op_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n)\n\nfunc init() {\n\tdB, err := gorm.Open(sqlite.Open(\"file::memory:?cache=shared\"), &gorm.Config{})\n\tif err != nil {\n\t\tpanic(\"failed to connect database\")\n\t}\n\tconf.Conf = conf.DefaultConfig(\"data\")\n\tdb.Init(dB)\n}\n\nfunc TestCreateStorage(t *testing.T) {\n\tvar storages = []struct {\n\t\tstorage model.Storage\n\t\tisErr   bool\n\t}{\n\t\t{storage: model.Storage{Driver: \"Local\", MountPath: \"/local\", Addition: `{\"root_folder_path\":\".\"}`}, isErr: false},\n\t\t{storage: model.Storage{Driver: \"Local\", MountPath: \"/local\", Addition: `{\"root_folder_path\":\".\"}`}, isErr: true},\n\t\t{storage: model.Storage{Driver: \"None\", MountPath: \"/none\", Addition: `{\"root_folder_path\":\".\"}`}, isErr: true},\n\t}\n\tfor _, storage := range storages {\n\t\t_, err := op.CreateStorage(context.Background(), storage.storage)\n\t\tif err != nil {\n\t\t\tif !storage.isErr {\n\t\t\t\tt.Errorf(\"failed to create storage: %+v\", err)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"expect failed to create storage: %+v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestGetStorageVirtualFilesByPath(t *testing.T) {\n\tsetupStorages(t)\n\tvirtualFiles := op.GetStorageVirtualFilesByPath(\"/a\")\n\tvar names []string\n\tfor _, virtualFile := range virtualFiles {\n\t\tnames = append(names, virtualFile.GetName())\n\t}\n\tvar expectedNames = []string{\"b\", \"c\", \"d\"}\n\tif utils.SliceEqual(names, expectedNames) {\n\t\tt.Logf(\"passed\")\n\t} else {\n\t\tt.Errorf(\"expected: %+v, got: %+v\", expectedNames, names)\n\t}\n}\n\nfunc TestGetBalancedStorage(t *testing.T) {\n\tset := mapset.NewSet[string]()\n\tfor i := 0; i < 5; i++ {\n\t\tstorage := op.GetBalancedStorage(\"/a/d/e1\")\n\t\tset.Add(storage.GetStorage().MountPath)\n\t}\n\texpected := mapset.NewSet([]string{\"/a/d/e1\", \"/a/d/e1.balance\"}...)\n\tif !expected.Equal(set) {\n\t\tt.Errorf(\"expected: %+v, got: %+v\", expected, set)\n\t}\n}\n\nfunc setupStorages(t *testing.T) {\n\tvar storages = []model.Storage{\n\t\t{Driver: \"Local\", MountPath: \"/a/b\", Order: 0, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/adc\", Order: 0, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/a/c\", Order: 1, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/a/d\", Order: 2, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/a/d/e1\", Order: 3, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/a/d/e\", Order: 4, Addition: `{\"root_folder_path\":\".\"}`},\n\t\t{Driver: \"Local\", MountPath: \"/a/d/e1.balance\", Order: 4, Addition: `{\"root_folder_path\":\".\"}`},\n\t}\n\tfor _, storage := range storages {\n\t\t_, err := op.CreateStorage(context.Background(), storage)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create storage: %+v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/op/user.go",
    "content": "package op\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/singleflight\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nvar userG singleflight.Group[*model.User]\nvar guestUser *model.User\nvar adminUser *model.User\n\nfunc GetAdmin() (*model.User, error) {\n\tif adminUser == nil {\n\t\tuser, err := db.GetUserByRole(model.ADMIN)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tadminUser = user\n\t}\n\treturn adminUser, nil\n}\n\nfunc GetGuest() (*model.User, error) {\n\tif guestUser == nil {\n\t\tuser, err := db.GetUserByRole(model.GUEST)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tguestUser = user\n\t}\n\treturn guestUser, nil\n}\n\nfunc GetUserByRole(role int) (*model.User, error) {\n\treturn db.GetUserByRole(role)\n}\n\nfunc GetUserByName(username string) (*model.User, error) {\n\tif username == \"\" {\n\t\treturn nil, errs.EmptyUsername\n\t}\n\tif user, exists := Cache.GetUser(username); exists {\n\t\treturn user, nil\n\t}\n\tuser, err, _ := userG.Do(username, func() (*model.User, error) {\n\t\t_user, err := db.GetUserByName(username)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tCache.SetUser(username, _user)\n\t\treturn _user, nil\n\t})\n\treturn user, err\n}\n\nfunc GetUserById(id uint) (*model.User, error) {\n\treturn db.GetUserById(id)\n}\n\nfunc GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err error) {\n\treturn db.GetUsers(pageIndex, pageSize)\n}\n\nfunc CreateUser(u *model.User) error {\n\tu.BasePath = utils.FixAndCleanPath(u.BasePath)\n\treturn db.CreateUser(u)\n}\n\nfunc DeleteUserById(id uint) error {\n\told, err := db.GetUserById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif old.IsAdmin() || old.IsGuest() {\n\t\treturn errs.DeleteAdminOrGuest\n\t}\n\tCache.DeleteUser(old.Username)\n\tif err := DeleteSharingsByCreatorId(id); err != nil {\n\t\treturn errors.WithMessage(err, \"failed to delete user's sharings\")\n\t}\n\treturn db.DeleteUserById(id)\n}\n\nfunc UpdateUser(u *model.User) error {\n\told, err := db.GetUserById(u.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif u.IsAdmin() {\n\t\tadminUser = nil\n\t}\n\tif u.IsGuest() {\n\t\tguestUser = nil\n\t}\n\tCache.DeleteUser(old.Username)\n\tu.BasePath = utils.FixAndCleanPath(u.BasePath)\n\treturn db.UpdateUser(u)\n}\n\nfunc Cancel2FAByUser(u *model.User) error {\n\tu.OtpSecret = \"\"\n\treturn UpdateUser(u)\n}\n\nfunc Cancel2FAById(id uint) error {\n\tuser, err := db.GetUserById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn Cancel2FAByUser(user)\n}\n\nfunc DelUserCache(username string) error {\n\tuser, err := GetUserByName(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user.IsAdmin() {\n\t\tadminUser = nil\n\t}\n\tif user.IsGuest() {\n\t\tguestUser = nil\n\t}\n\tCache.DeleteUser(username)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/search/bleve/init.go",
    "content": "package bleve\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\t\"github.com/blevesearch/bleve/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar config = searcher.Config{\n\tName: \"bleve\",\n}\n\nfunc Init(indexPath *string) (bleve.Index, error) {\n\tlog.Debugf(\"bleve path: %s\", *indexPath)\n\tfileIndex, err := bleve.Open(*indexPath)\n\tif err == bleve.ErrorIndexPathDoesNotExist {\n\t\tlog.Infof(\"Creating new index...\")\n\t\tindexMapping := bleve.NewIndexMapping()\n\t\tsearchNodeMapping := bleve.NewDocumentMapping()\n\t\tsearchNodeMapping.AddFieldMappingsAt(\"is_dir\", bleve.NewBooleanFieldMapping())\n\t\t// TODO: appoint analyzer\n\t\tparentFieldMapping := bleve.NewTextFieldMapping()\n\t\tsearchNodeMapping.AddFieldMappingsAt(\"parent\", parentFieldMapping)\n\t\t// TODO: appoint analyzer\n\t\tnameFieldMapping := bleve.NewKeywordFieldMapping()\n\t\tsearchNodeMapping.AddFieldMappingsAt(\"name\", nameFieldMapping)\n\t\tindexMapping.AddDocumentMapping(\"SearchNode\", searchNodeMapping)\n\t\tfileIndex, err = bleve.New(*indexPath, indexMapping)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\treturn fileIndex, nil\n}\n\nfunc init() {\n\tsearcher.RegisterSearcher(config, func() (searcher.Searcher, error) {\n\t\tb, err := Init(&conf.Conf.BleveDir)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &Bleve{BIndex: b}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/search/bleve/search.go",
    "content": "package bleve\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\tquery2 \"github.com/blevesearch/bleve/v2/search/query\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/blevesearch/bleve/v2\"\n\tsearch2 \"github.com/blevesearch/bleve/v2/search\"\n\t\"github.com/google/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Bleve struct {\n\tBIndex bleve.Index\n}\n\nfunc (b *Bleve) Config() searcher.Config {\n\treturn config\n}\n\nfunc (b *Bleve) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {\n\tvar queries []query2.Query\n\tquery := bleve.NewMatchQuery(req.Keywords)\n\tquery.SetField(\"name\")\n\tqueries = append(queries, query)\n\tif req.Scope != 0 {\n\t\tisDir := req.Scope == 1\n\t\tisDirQuery := bleve.NewBoolFieldQuery(isDir)\n\t\tqueries = append(queries, isDirQuery)\n\t}\n\treqQuery := bleve.NewConjunctionQuery(queries...)\n\tsearch := bleve.NewSearchRequest(reqQuery)\n\tsearch.SortBy([]string{\"name\"})\n\tsearch.From = (req.Page - 1) * req.PerPage\n\tsearch.Size = req.PerPage\n\tsearch.Fields = []string{\"*\"}\n\tsearchResults, err := b.BIndex.Search(search)\n\tif err != nil {\n\t\tlog.Errorf(\"search error: %+v\", err)\n\t\treturn nil, 0, err\n\t}\n\tres, err := utils.SliceConvert(searchResults.Hits, func(src *search2.DocumentMatch) (model.SearchNode, error) {\n\t\treturn model.SearchNode{\n\t\t\tParent: src.Fields[\"parent\"].(string),\n\t\t\tName:   src.Fields[\"name\"].(string),\n\t\t\tIsDir:  src.Fields[\"is_dir\"].(bool),\n\t\t\tSize:   int64(src.Fields[\"size\"].(float64)),\n\t\t}, nil\n\t})\n\treturn res, int64(searchResults.Total), nil\n}\n\nfunc (b *Bleve) Index(ctx context.Context, node model.SearchNode) error {\n\treturn b.BIndex.Index(uuid.NewString(), node)\n}\n\nfunc (b *Bleve) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {\n\tbatch := b.BIndex.NewBatch()\n\tfor _, node := range nodes {\n\t\tbatch.Index(uuid.NewString(), node)\n\t}\n\treturn b.BIndex.Batch(batch)\n}\n\nfunc (b *Bleve) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (b *Bleve) Del(ctx context.Context, prefix string) error {\n\treturn errs.NotSupport\n}\n\nfunc (b *Bleve) Release(ctx context.Context) error {\n\tif b.BIndex != nil {\n\t\treturn b.BIndex.Close()\n\t}\n\treturn nil\n}\n\nfunc (b *Bleve) Clear(ctx context.Context) error {\n\terr := b.Release(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Infof(\"Removing old index...\")\n\terr = os.RemoveAll(conf.Conf.BleveDir)\n\tif err != nil {\n\t\tlog.Errorf(\"clear bleve error: %+v\", err)\n\t}\n\tbIndex, err := Init(&conf.Conf.BleveDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tb.BIndex = bIndex\n\treturn nil\n}\n\nvar _ searcher.Searcher = (*Bleve)(nil)\n"
  },
  {
    "path": "internal/search/build.go",
    "content": "package search\n\nimport (\n\t\"context\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/mq\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tQuit = atomic.Pointer[chan struct{}]{}\n)\n\nfunc Running() bool {\n\treturn Quit.Load() != nil\n}\n\nfunc BuildIndex(ctx context.Context, indexPaths, ignorePaths []string, maxDepth int, count bool) error {\n\tvar (\n\t\terr      error\n\t\tobjCount uint64 = 0\n\t\tfi       model.Obj\n\t)\n\tlog.Infof(\"build index for: %+v\", indexPaths)\n\tlog.Infof(\"ignore paths: %+v\", ignorePaths)\n\tquit := make(chan struct{}, 1)\n\tif !Quit.CompareAndSwap(nil, &quit) {\n\t\t// other goroutine is running\n\t\treturn errs.BuildIndexIsRunning\n\t}\n\tvar (\n\t\tindexMQ = mq.NewInMemoryMQ[ObjWithParent]()\n\t\trunning = atomic.Bool{} // current goroutine running\n\t\twg      = &sync.WaitGroup{}\n\t)\n\trunning.Store(true)\n\twg.Add(1)\n\tgo func() {\n\t\tticker := time.NewTicker(time.Second)\n\t\tdefer func() {\n\t\t\tQuit.Store(nil)\n\t\t\twg.Done()\n\t\t\t// notify walk to exit when StopIndex api called\n\t\t\trunning.Store(false)\n\t\t\tticker.Stop()\n\t\t}()\n\t\ttickCount := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\ttickCount += 1\n\t\t\t\tif indexMQ.Len() < 1000 && tickCount != 5 {\n\t\t\t\t\tcontinue\n\t\t\t\t} else if tickCount >= 5 {\n\t\t\t\t\ttickCount = 0\n\t\t\t\t}\n\t\t\t\tlog.Infof(\"index obj count: %d\", objCount)\n\t\t\t\tindexMQ.ConsumeAll(func(messages []mq.Message[ObjWithParent]) {\n\t\t\t\t\tif len(messages) != 0 {\n\t\t\t\t\t\tlog.Debugf(\"current index: %s\", messages[len(messages)-1].Content.Parent)\n\t\t\t\t\t}\n\t\t\t\t\tif err = BatchIndex(ctx, utils.MustSliceConvert(messages,\n\t\t\t\t\t\tfunc(src mq.Message[ObjWithParent]) ObjWithParent {\n\t\t\t\t\t\t\treturn src.Content\n\t\t\t\t\t\t})); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"build index in batch error: %+v\", err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tobjCount = objCount + uint64(len(messages))\n\t\t\t\t\t}\n\t\t\t\t\tif count {\n\t\t\t\t\t\tWriteProgress(&model.IndexProgress{\n\t\t\t\t\t\t\tObjCount:     objCount,\n\t\t\t\t\t\t\tIsDone:       false,\n\t\t\t\t\t\t\tLastDoneTime: nil,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\tcase <-quit:\n\t\t\t\tlog.Debugf(\"build index for %+v received quit\", indexPaths)\n\t\t\t\teMsg := \"\"\n\t\t\t\tnow := time.Now()\n\t\t\t\toriginErr := err\n\t\t\t\tindexMQ.ConsumeAll(func(messages []mq.Message[ObjWithParent]) {\n\t\t\t\t\tif err = BatchIndex(ctx, utils.MustSliceConvert(messages,\n\t\t\t\t\t\tfunc(src mq.Message[ObjWithParent]) ObjWithParent {\n\t\t\t\t\t\t\treturn src.Content\n\t\t\t\t\t\t})); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"build index in batch error: %+v\", err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tobjCount = objCount + uint64(len(messages))\n\t\t\t\t\t}\n\t\t\t\t\tif originErr != nil {\n\t\t\t\t\t\tlog.Errorf(\"build index error: %+v\", originErr)\n\t\t\t\t\t\teMsg = originErr.Error()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Infof(\"success build index, count: %d\", objCount)\n\t\t\t\t\t}\n\t\t\t\t\tif count {\n\t\t\t\t\t\tWriteProgress(&model.IndexProgress{\n\t\t\t\t\t\t\tObjCount:     objCount,\n\t\t\t\t\t\t\tIsDone:       true,\n\t\t\t\t\t\t\tLastDoneTime: &now,\n\t\t\t\t\t\t\tError:        eMsg,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\tlog.Debugf(\"build index for %+v quit success\", indexPaths)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\tdefer func() {\n\t\tif !running.Load() || Quit.Load() != &quit {\n\t\t\tlog.Debugf(\"build index for %+v stopped by StopIndex\", indexPaths)\n\t\t\treturn\n\t\t}\n\t\tselect {\n\t\t// avoid goroutine leak\n\t\tcase quit <- struct{}{}:\n\t\tdefault:\n\t\t}\n\t\twg.Wait()\n\t}()\n\tadmin, err := op.GetAdmin()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif count {\n\t\tWriteProgress(&model.IndexProgress{\n\t\t\tObjCount: 0,\n\t\t\tIsDone:   false,\n\t\t})\n\t}\n\tfor _, indexPath := range indexPaths {\n\t\twalkFn := func(indexPath string, info model.Obj) error {\n\t\t\tif !running.Load() {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\tfor _, avoidPath := range ignorePaths {\n\t\t\t\tif strings.HasPrefix(indexPath, avoidPath) {\n\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t}\n\t\t\t}\n\t\t\tif storage, _, err := op.GetStorageAndActualPath(indexPath); err == nil {\n\t\t\t\tif storage.GetStorage().DisableIndex {\n\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t}\n\t\t\t}\n\t\t\t// ignore root\n\t\t\tif indexPath == \"/\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tindexMQ.Publish(mq.Message[ObjWithParent]{\n\t\t\t\tContent: ObjWithParent{\n\t\t\t\t\tObj:    info,\n\t\t\t\t\tParent: path.Dir(indexPath),\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\t\tfi, err = fs.Get(ctx, indexPath, &fs.GetArgs{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// TODO: run walkFS concurrently\n\t\terr = fs.WalkFS(context.WithValue(ctx, conf.UserKey, admin), maxDepth, indexPath, fi, walkFn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc Del(ctx context.Context, prefix string) error {\n\treturn instance.Del(ctx, prefix)\n}\n\nfunc Clear(ctx context.Context) error {\n\treturn instance.Clear(ctx)\n}\n\nfunc Config(ctx context.Context) searcher.Config {\n\treturn instance.Config()\n}\n\nfunc Update(ctx context.Context, parent string, objs []model.Obj) {\n\tif instance == nil || !instance.Config().AutoUpdate || !setting.GetBool(conf.AutoUpdateIndex) || Running() {\n\t\treturn\n\t}\n\tif isIgnorePath(parent) {\n\t\treturn\n\t}\n\t// only update when index have built\n\tprogress, err := Progress()\n\tif err != nil {\n\t\tlog.Errorf(\"update search index error while get progress: %+v\", err)\n\t\treturn\n\t}\n\tif !progress.IsDone {\n\t\treturn\n\t}\n\n\t// Use task queue for Meilisearch to avoid race conditions with async indexing\n\tif msInstance, ok := instance.(interface {\n\t\tEnqueueUpdate(parent string, objs []model.Obj)\n\t}); ok {\n\t\t// Enqueue task for async processing (diff calculation happens at consumption time)\n\t\tmsInstance.EnqueueUpdate(parent, objs)\n\t\treturn\n\t}\n\n\tnodes, err := instance.Get(ctx, parent)\n\tif err != nil {\n\t\tlog.Errorf(\"update search index error while get nodes: %+v\", err)\n\t\treturn\n\t}\n\tnow := mapset.NewSet[string]()\n\tfor i := range objs {\n\t\tnow.Add(objs[i].GetName())\n\t}\n\told := mapset.NewSet[string]()\n\tfor i := range nodes {\n\t\told.Add(nodes[i].Name)\n\t}\n\t// delete data that no longer exists\n\ttoDelete := old.Difference(now)\n\ttoAdd := now.Difference(old)\n\tfor i := range nodes {\n\t\tif toDelete.Contains(nodes[i].Name) && !op.HasStorage(path.Join(parent, nodes[i].Name)) {\n\t\t\tlog.Debugf(\"delete index: %s\", path.Join(parent, nodes[i].Name))\n\t\t\terr = instance.Del(ctx, path.Join(parent, nodes[i].Name))\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"update search index error while del old node: %+v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// collect files and folders to add in batch\n\tvar toAddObjs []ObjWithParent\n\tfor i := range objs {\n\t\tif toAdd.Contains(objs[i].GetName()) {\n\t\t\tlog.Debugf(\"add index: %s\", path.Join(parent, objs[i].GetName()))\n\t\t\ttoAddObjs = append(toAddObjs, ObjWithParent{\n\t\t\t\tParent: parent,\n\t\t\t\tObj:    objs[i],\n\t\t\t})\n\t\t}\n\t}\n\t// batch index all files and folders at once\n\tif len(toAddObjs) > 0 {\n\t\terr = BatchIndex(ctx, toAddObjs)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"update search index error while batch index new nodes: %+v\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc init() {\n\top.RegisterObjsUpdateHook(Update)\n}\n"
  },
  {
    "path": "internal/search/db/init.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n)\n\nvar config = searcher.Config{\n\tName:       \"database\",\n\tAutoUpdate: true,\n}\n\nfunc init() {\n\tsearcher.RegisterSearcher(config, func() (searcher.Searcher, error) {\n\t\tdb := db.GetDb()\n\t\tswitch conf.Conf.Database.Type {\n\t\tcase \"mysql\":\n\t\t\ttableName := fmt.Sprintf(\"%ssearch_nodes\", conf.Conf.Database.TablePrefix)\n\t\t\ttx := db.Exec(fmt.Sprintf(\"CREATE FULLTEXT INDEX idx_%s_name_fulltext ON %s(name);\", tableName, tableName))\n\t\t\tif err := tx.Error; err != nil && !strings.Contains(err.Error(), \"Error 1061 (42000)\") { // duplicate error\n\t\t\t\tlog.Errorf(\"failed to create full text index: %v\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase \"postgres\":\n\t\t\tdb.Exec(\"CREATE EXTENSION pg_trgm;\")\n\t\t\tdb.Exec(\"CREATE EXTENSION btree_gin;\")\n\t\t\ttableName := fmt.Sprintf(\"%ssearch_nodes\", conf.Conf.Database.TablePrefix)\n\t\t\ttx := db.Exec(fmt.Sprintf(\"CREATE INDEX idx_%s_name ON %s USING GIN (name);\", tableName, tableName))\n\t\t\tif err := tx.Error; err != nil && !strings.Contains(err.Error(), \"SQLSTATE 42P07\") {\n\t\t\t\tlog.Errorf(\"failed to create index using GIN: %v\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn &DB{}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/search/db/search.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n)\n\ntype DB struct{}\n\nfunc (D DB) Config() searcher.Config {\n\treturn config\n}\n\nfunc (D DB) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {\n\treturn db.SearchNode(req, true)\n}\n\nfunc (D DB) Index(ctx context.Context, node model.SearchNode) error {\n\treturn db.CreateSearchNode(&node)\n}\n\nfunc (D DB) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {\n\treturn db.BatchCreateSearchNodes(&nodes)\n}\n\nfunc (D DB) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {\n\treturn db.GetSearchNodesByParent(parent)\n}\n\nfunc (D DB) Del(ctx context.Context, path string) error {\n\treturn db.DeleteSearchNodesByParent(path)\n}\n\nfunc (D DB) Release(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (D DB) Clear(ctx context.Context) error {\n\treturn db.ClearSearchNodes()\n}\n\nvar _ searcher.Searcher = (*DB)(nil)\n"
  },
  {
    "path": "internal/search/db_non_full_text/init.go",
    "content": "package db_non_full_text\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n)\n\nvar config = searcher.Config{\n\tName:       \"database_non_full_text\",\n\tAutoUpdate: true,\n}\n\nfunc init() {\n\tsearcher.RegisterSearcher(config, func() (searcher.Searcher, error) {\n\t\treturn &DB{}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/search/db_non_full_text/search.go",
    "content": "package db_non_full_text\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n)\n\ntype DB struct{}\n\nfunc (D DB) Config() searcher.Config {\n\treturn config\n}\n\nfunc (D DB) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {\n\treturn db.SearchNode(req, false)\n}\n\nfunc (D DB) Index(ctx context.Context, node model.SearchNode) error {\n\treturn db.CreateSearchNode(&node)\n}\n\nfunc (D DB) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {\n\treturn db.BatchCreateSearchNodes(&nodes)\n}\n\nfunc (D DB) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {\n\treturn db.GetSearchNodesByParent(parent)\n}\n\nfunc (D DB) Del(ctx context.Context, path string) error {\n\treturn db.DeleteSearchNodesByParent(path)\n}\n\nfunc (D DB) Release(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (D DB) Clear(ctx context.Context) error {\n\treturn db.ClearSearchNodes()\n}\n\nvar _ searcher.Searcher = (*DB)(nil)\n"
  },
  {
    "path": "internal/search/import.go",
    "content": "package search\n\nimport (\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/search/bleve\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/search/db\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/search/db_non_full_text\"\n\t_ \"github.com/OpenListTeam/OpenList/v4/internal/search/meilisearch\"\n)\n"
  },
  {
    "path": "internal/search/meilisearch/init.go",
    "content": "package meilisearch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/meilisearch/meilisearch-go\"\n)\n\nvar config = searcher.Config{\n\tName:       \"meilisearch\",\n\tAutoUpdate: true,\n}\n\nfunc init() {\n\tsearcher.RegisterSearcher(config, func() (searcher.Searcher, error) {\n\t\tindexUid := conf.Conf.Meilisearch.Index\n\t\tif len(indexUid) == 0 {\n\t\t\treturn nil, errors.New(\"index is blank\")\n\t\t}\n\t\tm := Meilisearch{\n\t\t\tClient: meilisearch.New(\n\t\t\t\tconf.Conf.Meilisearch.Host,\n\t\t\t\tmeilisearch.WithAPIKey(conf.Conf.Meilisearch.APIKey),\n\t\t\t),\n\t\t\tIndexUid: indexUid,\n\t\t\tFilterableAttributes: []string{\"parent\", \"is_dir\", \"name\",\n\t\t\t\t\"parent_hash\", \"parent_path_hashes\"},\n\t\t\tSearchableAttributes: []string{\"name\"},\n\t\t}\n\n\t\t_, err := m.Client.GetIndex(m.IndexUid)\n\t\tif err != nil {\n\t\t\tvar mErr *meilisearch.Error\n\t\t\tok := errors.As(err, &mErr)\n\t\t\tif ok && mErr.MeilisearchApiError.Code == \"index_not_found\" {\n\t\t\t\ttask, err := m.Client.CreateIndex(&meilisearch.IndexConfig{\n\t\t\t\t\tUid:        m.IndexUid,\n\t\t\t\t\tPrimaryKey: \"id\",\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tforTask, err := m.Client.WaitForTask(task.TaskUID, time.Second)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif forTask.Status != meilisearch.TaskStatusSucceeded {\n\t\t\t\t\treturn nil, fmt.Errorf(\"index creation failed, task status is %s\", forTask.Status)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tattributes, err := m.Client.Index(m.IndexUid).GetFilterableAttributes()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif attributes == nil || !utils.SliceAllContains(*attributes, m.FilterableAttributes...) {\n\t\t\t_, err = m.Client.Index(m.IndexUid).UpdateFilterableAttributes(&m.FilterableAttributes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tattributes, err = m.Client.Index(m.IndexUid).GetSearchableAttributes()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif attributes == nil || !utils.SliceAllContains(*attributes, m.SearchableAttributes...) {\n\t\t\t_, err = m.Client.Index(m.IndexUid).UpdateSearchableAttributes(&m.SearchableAttributes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tpagination, err := m.Client.Index(m.IndexUid).GetPagination()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif pagination.MaxTotalHits != int64(model.MaxInt) {\n\t\t\t_, err := m.Client.Index(m.IndexUid).UpdatePagination(&meilisearch.Pagination{\n\t\t\t\tMaxTotalHits: int64(model.MaxInt),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\t// Initialize and start task queue manager\n\t\tm.taskQueue = NewTaskQueueManager(&m)\n\t\tm.taskQueue.Start()\n\n\t\treturn &m, nil\n\t})\n}\n"
  },
  {
    "path": "internal/search/meilisearch/search.go",
    "content": "package meilisearch\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/meilisearch/meilisearch-go\"\n)\n\ntype searchDocument struct {\n\t// Document id, hash of the file path,\n\t// can be used for filtering a file exactly(case-sensitively).\n\tID string `json:\"id\"`\n\t// Hash of parent, can be used for filtering direct children.\n\tParentHash string `json:\"parent_hash\"`\n\t// One-by-one hash of parent paths (path hierarchy).\n\t// eg: A file's parent is '/home/a/b',\n\t// its parent paths are '/home/a/b', '/home/a', '/home', '/'.\n\t// Can be used for filtering all descendants exactly.\n\t// Storing path hashes instead of plaintext paths benefits disk usage and case-sensitive filter.\n\tParentPathHashes []string `json:\"parent_path_hashes\"`\n\tmodel.SearchNode\n}\n\ntype Meilisearch struct {\n\tClient               meilisearch.ServiceManager\n\tIndexUid             string\n\tFilterableAttributes []string\n\tSearchableAttributes []string\n\ttaskQueue            *TaskQueueManager\n}\n\nfunc (m *Meilisearch) Config() searcher.Config {\n\treturn config\n}\n\nfunc (m *Meilisearch) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {\n\tmReq := &meilisearch.SearchRequest{\n\t\tAttributesToSearchOn: m.SearchableAttributes,\n\t\tPage:                 int64(req.Page),\n\t\tHitsPerPage:          int64(req.PerPage),\n\t}\n\tvar filters []string\n\tif req.Scope != 0 {\n\t\tfilters = append(filters, fmt.Sprintf(\"is_dir = %v\", req.Scope == 1))\n\t}\n\tif req.Parent != \"\" && req.Parent != \"/\" {\n\t\t// use parent_path_hashes to filter descendants\n\t\tparentHash := hashPath(req.Parent)\n\t\tfilters = append(filters, fmt.Sprintf(\"parent_path_hashes = '%s'\", parentHash))\n\t}\n\tif len(filters) > 0 {\n\t\tmReq.Filter = strings.Join(filters, \" AND \")\n\t}\n\n\tsearch, err := m.Client.Index(m.IndexUid).SearchWithContext(ctx, req.Keywords, mReq)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tnodes, err := utils.SliceConvert(search.Hits, func(src any) (model.SearchNode, error) {\n\t\tsrcMap := src.(map[string]any)\n\t\treturn model.SearchNode{\n\t\t\tParent: srcMap[\"parent\"].(string),\n\t\t\tName:   srcMap[\"name\"].(string),\n\t\t\tIsDir:  srcMap[\"is_dir\"].(bool),\n\t\t\tSize:   int64(srcMap[\"size\"].(float64)),\n\t\t}, nil\n\t})\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn nodes, search.TotalHits, nil\n}\n\nfunc (m *Meilisearch) Index(ctx context.Context, node model.SearchNode) error {\n\treturn m.BatchIndex(ctx, []model.SearchNode{node})\n}\n\nfunc (m *Meilisearch) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {\n\tdocuments, err := utils.SliceConvert(nodes, func(src model.SearchNode) (*searchDocument, error) {\n\t\tparentHash := hashPath(src.Parent)\n\t\tnodePath := path.Join(src.Parent, src.Name)\n\t\tnodePathHash := hashPath(nodePath)\n\t\tparentPaths := utils.GetPathHierarchy(src.Parent)\n\t\tparentPathHashes, err := utils.SliceConvert(parentPaths, func(parentPath string) (string, error) {\n\t\t\treturn hashPath(parentPath), nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &searchDocument{\n\t\t\tID:               nodePathHash,\n\t\t\tParentHash:       parentHash,\n\t\t\tParentPathHashes: parentPathHashes,\n\t\t\tSearchNode:       src,\n\t\t}, nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// max up to 10,000 documents per batch to reduce error rate while uploading over the Internet\n\t_, err = m.Client.Index(m.IndexUid).AddDocumentsInBatchesWithContext(ctx, documents, 10000)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// documents were uploaded and enqueued for indexing, just return early\n\t//// Wait for the task to complete and check\n\t//forTask, err := m.Client.WaitForTask(task.TaskUID, meilisearch.WaitParams{\n\t//\tContext:  ctx,\n\t//\tInterval: time.Millisecond * 50,\n\t//})\n\t//if err != nil {\n\t//\treturn err\n\t//}\n\t//if forTask.Status != meilisearch.TaskStatusSucceeded {\n\t//\treturn fmt.Errorf(\"BatchIndex failed, task status is %s\", forTask.Status)\n\t//}\n\treturn nil\n}\n\nfunc (m *Meilisearch) getDocumentsByParent(ctx context.Context, parent string) ([]*searchDocument, error) {\n\tvar result meilisearch.DocumentsResult\n\tquery := &meilisearch.DocumentsQuery{\n\t\tLimit: int64(model.MaxInt),\n\t}\n\tif parent != \"\" && parent != \"/\" {\n\t\t// use parent_hash to filter direct children\n\t\tparentHash := hashPath(parent)\n\t\tquery.Filter = fmt.Sprintf(\"parent_hash = '%s'\", parentHash)\n\t}\n\terr := m.Client.Index(m.IndexUid).GetDocumentsWithContext(ctx, query, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(result.Results, func(src map[string]any) (*searchDocument, error) {\n\t\treturn buildSearchDocumentFromResults(src), nil\n\t})\n}\n\nfunc (m *Meilisearch) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {\n\tresult, err := m.getDocumentsByParent(ctx, parent)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn utils.SliceConvert(result, func(src *searchDocument) (model.SearchNode, error) {\n\t\treturn src.SearchNode, nil\n\t})\n}\n\nfunc (m *Meilisearch) getDocumentInPath(ctx context.Context, parent string, name string) (*searchDocument, error) {\n\tvar result searchDocument\n\t// join them and calculate the hash to exactly identify the node\n\tnodePath := path.Join(parent, name)\n\tnodePathHash := hashPath(nodePath)\n\terr := m.Client.Index(m.IndexUid).GetDocumentWithContext(ctx, nodePathHash, nil, &result)\n\tif err != nil {\n\t\t// return nil for documents that no exists\n\t\tif err.(*meilisearch.Error).StatusCode == 404 {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &result, nil\n}\n\nfunc (m *Meilisearch) delDirChild(ctx context.Context, prefix string) error {\n\tprefix = hashPath(prefix)\n\t// use parent_path_hashes to filter descendants,\n\t// so no longer need to walk through the directories to get their IDs,\n\t// speeding up the deletion process with easy maintained codebase\n\tfilter := fmt.Sprintf(\"parent_path_hashes = '%s'\", prefix)\n\t_, err := m.Client.Index(m.IndexUid).DeleteDocumentsByFilterWithContext(ctx, filter)\n\t// task was enqueued (if succeed), no need to wait\n\treturn err\n}\n\nfunc (m *Meilisearch) Del(ctx context.Context, prefix string) error {\n\tprefix = utils.FixAndCleanPath(prefix)\n\tdir, name := path.Split(prefix)\n\tif dir != \"/\" {\n\t\tdir = dir[:len(dir)-1]\n\t}\n\n\tdocument, err := m.getDocumentInPath(ctx, dir, name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif document == nil {\n\t\t// Defensive programming. Document may be the folder, try deleting Child\n\t\treturn m.delDirChild(ctx, prefix)\n\t}\n\tif document.IsDir {\n\t\terr = m.delDirChild(ctx, prefix)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err = m.Client.Index(m.IndexUid).DeleteDocumentWithContext(ctx, document.ID)\n\t// task was enqueued (if succeed), no need to wait\n\treturn err\n}\n\nfunc (m *Meilisearch) Release(ctx context.Context) error {\n\tif m.taskQueue != nil {\n\t\tm.taskQueue.Stop()\n\t}\n\treturn nil\n}\n\nfunc (m *Meilisearch) Clear(ctx context.Context) error {\n\t_, err := m.Client.Index(m.IndexUid).DeleteAllDocumentsWithContext(ctx)\n\t// task was enqueued (if succeed), no need to wait\n\treturn err\n}\n\nfunc (m *Meilisearch) getTaskStatus(ctx context.Context, taskUID int64) (meilisearch.TaskStatus, error) {\n\tforTask, err := m.Client.WaitForTaskWithContext(ctx, taskUID, time.Second)\n\tif err != nil {\n\t\treturn meilisearch.TaskStatusUnknown, err\n\t}\n\treturn forTask.Status, nil\n}\n\n// EnqueueUpdate enqueues an update task to the task queue\nfunc (m *Meilisearch) EnqueueUpdate(parent string, objs []model.Obj) {\n\tif m.taskQueue == nil {\n\t\treturn\n\t}\n\n\tm.taskQueue.Enqueue(parent, objs)\n}\n\n// batchIndexWithTaskUID indexes documents and returns all taskUIDs\nfunc (m *Meilisearch) batchIndexWithTaskUID(ctx context.Context, nodes []model.SearchNode) ([]int64, error) {\n\tif len(nodes) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tdocuments, err := utils.SliceConvert(nodes, func(src model.SearchNode) (*searchDocument, error) {\n\t\tparentHash := hashPath(src.Parent)\n\t\tnodePath := path.Join(src.Parent, src.Name)\n\t\tnodePathHash := hashPath(nodePath)\n\t\tparentPaths := utils.GetPathHierarchy(src.Parent)\n\t\tparentPathHashes, err := utils.SliceConvert(parentPaths, func(parentPath string) (string, error) {\n\t\t\treturn hashPath(parentPath), nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &searchDocument{\n\t\t\tID:               nodePathHash,\n\t\t\tParentHash:       parentHash,\n\t\t\tParentPathHashes: parentPathHashes,\n\t\t\tSearchNode:       src,\n\t\t}, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// max up to 10,000 documents per batch to reduce error rate while uploading over the Internet\n\ttasks, err := m.Client.Index(m.IndexUid).AddDocumentsInBatchesWithContext(ctx, documents, 10000)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Return all task UIDs\n\ttaskUIDs := make([]int64, 0, len(tasks))\n\tfor _, task := range tasks {\n\t\ttaskUIDs = append(taskUIDs, task.TaskUID)\n\t}\n\treturn taskUIDs, nil\n}\n\n// batchDeleteWithTaskUID deletes documents and returns all taskUIDs\nfunc (m *Meilisearch) batchDeleteWithTaskUID(ctx context.Context, paths []string) ([]int64, error) {\n\tif len(paths) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// Deduplicate paths first\n\tpathSet := make(map[string]struct{})\n\tuniquePaths := make([]string, 0, len(paths))\n\tfor _, p := range paths {\n\t\tp = utils.FixAndCleanPath(p)\n\t\tif _, exists := pathSet[p]; !exists {\n\t\t\tpathSet[p] = struct{}{}\n\t\t\tuniquePaths = append(uniquePaths, p)\n\t\t}\n\t}\n\n\tconst batchSize = 100 // max paths per batch to avoid filter length limits\n\tvar taskUIDs []int64\n\n\t// Process in batches to avoid filter length limits\n\tfor i := 0; i < len(uniquePaths); i += batchSize {\n\t\tend := i + batchSize\n\t\tif end > len(uniquePaths) {\n\t\t\tend = len(uniquePaths)\n\t\t}\n\t\tbatch := uniquePaths[i:end]\n\n\t\t// Build combined filter to delete all children in one request\n\t\t// Format: parent_path_hashes = 'hash1' OR parent_path_hashes = 'hash2' OR ...\n\t\tvar filters []string\n\t\tfor _, p := range batch {\n\t\t\tpathHash := hashPath(p)\n\t\t\tfilters = append(filters, fmt.Sprintf(\"parent_path_hashes = '%s'\", pathHash))\n\t\t}\n\t\tif len(filters) > 0 {\n\t\t\tcombinedFilter := strings.Join(filters, \" OR \")\n\t\t\t// Delete all children for all paths in one request\n\t\t\ttask, err := m.Client.Index(m.IndexUid).DeleteDocumentsByFilterWithContext(ctx, combinedFilter)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttaskUIDs = append(taskUIDs, task.TaskUID)\n\t\t}\n\n\t\t// Convert paths to document IDs and batch delete\n\t\tdocumentIDs := make([]string, 0, len(batch))\n\t\tfor _, p := range batch {\n\t\t\tdocumentIDs = append(documentIDs, hashPath(p))\n\t\t}\n\t\t// Use batch delete API\n\t\ttask, err := m.Client.Index(m.IndexUid).DeleteDocumentsWithContext(ctx, documentIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttaskUIDs = append(taskUIDs, task.TaskUID)\n\t}\n\treturn taskUIDs, nil\n}\n"
  },
  {
    "path": "internal/search/meilisearch/task_queue.go",
    "content": "package meilisearch\n\nimport (\n\t\"context\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// QueuedTask represents a task in the queue\ntype QueuedTask struct {\n\tParent    string\n\tObjs      []model.Obj // current file system state\n\tDepth     int         // path depth for sorting\n\tEnqueueAt time.Time   // enqueue time\n}\n\n// TaskQueueManager manages the task queue for async index operations\ntype TaskQueueManager struct {\n\tqueue        map[string]*QueuedTask // parent -> task\n\tpendingTasks map[string][]int64     // parent -> all submitted taskUIDs\n\tmu           sync.RWMutex\n\tticker       *time.Ticker\n\tstopCh       chan struct{}\n\tm            *Meilisearch\n\tconsuming    atomic.Bool // flag to prevent concurrent consumption\n}\n\n// NewTaskQueueManager creates a new task queue manager\nfunc NewTaskQueueManager(m *Meilisearch) *TaskQueueManager {\n\treturn &TaskQueueManager{\n\t\tqueue:        make(map[string]*QueuedTask),\n\t\tpendingTasks: make(map[string][]int64),\n\t\tstopCh:       make(chan struct{}),\n\t\tm:            m,\n\t}\n}\n\n// calculateDepth calculates the depth of a path\nfunc calculateDepth(path string) int {\n\tif path == \"/\" {\n\t\treturn 0\n\t}\n\treturn strings.Count(strings.Trim(path, \"/\"), \"/\") + 1\n}\n\n// Enqueue enqueues a task with current file system state\nfunc (tqm *TaskQueueManager) Enqueue(parent string, objs []model.Obj) {\n\ttqm.mu.Lock()\n\tdefer tqm.mu.Unlock()\n\n\t// deduplicate: overwrite existing task with the same parent\n\ttqm.queue[parent] = &QueuedTask{\n\t\tParent:    parent,\n\t\tObjs:      objs,\n\t\tDepth:     calculateDepth(parent),\n\t\tEnqueueAt: time.Now(),\n\t}\n\tlog.Debugf(\"enqueued update task for parent: %s, depth: %d, objs: %d\", parent, calculateDepth(parent), len(objs))\n}\n\n// Start starts the task queue consumer\nfunc (tqm *TaskQueueManager) Start() {\n\ttqm.ticker = time.NewTicker(30 * time.Second)\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-tqm.ticker.C:\n\t\t\t\ttqm.consume()\n\t\t\tcase <-tqm.stopCh:\n\t\t\t\tlog.Info(\"task queue manager stopped\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\tlog.Info(\"task queue manager started, will consume every 30 seconds\")\n}\n\n// Stop stops the task queue consumer\nfunc (tqm *TaskQueueManager) Stop() {\n\tif tqm.ticker != nil {\n\t\ttqm.ticker.Stop()\n\t}\n\tclose(tqm.stopCh)\n}\n\n// consume processes all tasks in the queue\nfunc (tqm *TaskQueueManager) consume() {\n\t// Prevent concurrent consumption\n\tif !tqm.consuming.CompareAndSwap(false, true) {\n\t\tlog.Warn(\"previous consume still running, skip this round\")\n\t\treturn\n\t}\n\tdefer tqm.consuming.Store(false)\n\n\ttqm.mu.Lock()\n\n\t// extract all tasks\n\ttasks := make([]*QueuedTask, 0, len(tqm.queue))\n\tfor _, task := range tqm.queue {\n\t\ttasks = append(tasks, task)\n\t}\n\n\t// clear queue\n\ttqm.queue = make(map[string]*QueuedTask)\n\n\ttqm.mu.Unlock()\n\n\tif len(tasks) == 0 {\n\t\treturn\n\t}\n\n\tlog.Infof(\"consuming task queue: %d tasks\", len(tasks))\n\n\t// sort tasks: shallow paths first, then by enqueue time\n\tsort.Slice(tasks, func(i, j int) bool {\n\t\tif tasks[i].Depth != tasks[j].Depth {\n\t\t\treturn tasks[i].Depth < tasks[j].Depth\n\t\t}\n\t\treturn tasks[i].EnqueueAt.Before(tasks[j].EnqueueAt)\n\t})\n\n\tctx := context.Background()\n\n\t// execute tasks in order\n\tfor _, task := range tasks {\n\t\t// Check if there are pending tasks for this parent\n\t\ttqm.mu.RLock()\n\t\tpendingTaskUIDs, hasPending := tqm.pendingTasks[task.Parent]\n\t\ttqm.mu.RUnlock()\n\n\t\tif hasPending && len(pendingTaskUIDs) > 0 {\n\t\t\t// Check all pending task statuses\n\t\t\tallCompleted := true\n\t\t\tfor _, taskUID := range pendingTaskUIDs {\n\t\t\t\ttaskStatus, err := tqm.m.getTaskStatus(ctx, taskUID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to get task status for parent %s (taskUID: %d): %v\", task.Parent, taskUID, err)\n\t\t\t\t\t// If we can't get status, assume it's done and continue checking\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Check if task is still running\n\t\t\t\tif taskStatus == \"enqueued\" || taskStatus == \"processing\" {\n\t\t\t\t\tlog.Warnf(\"skipping task for parent %s: previous task %d still %s\", task.Parent, taskUID, taskStatus)\n\t\t\t\t\tallCompleted = false\n\t\t\t\t\tbreak // No need to check remaining tasks\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !allCompleted {\n\t\t\t\t// Re-enqueue the task if not already in queue (avoid overwriting newer snapshots)\n\t\t\t\ttqm.mu.Lock()\n\t\t\t\tif _, exists := tqm.queue[task.Parent]; !exists {\n\t\t\t\t\ttqm.queue[task.Parent] = task\n\t\t\t\t\tlog.Debugf(\"re-enqueued skipped task for parent %s due to pending tasks\", task.Parent)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Debugf(\"skipped task for parent %s not re-enqueued (newer task already in queue)\", task.Parent)\n\t\t\t\t}\n\t\t\t\ttqm.mu.Unlock()\n\t\t\t\tcontinue // Skip this task, some previous tasks are still running\n\t\t\t}\n\n\t\t\t// All tasks are in terminal state, remove from pending\n\t\t\tlog.Debugf(\"all previous tasks for parent %s are completed, proceeding with new task\", task.Parent)\n\t\t\ttqm.mu.Lock()\n\t\t\tdelete(tqm.pendingTasks, task.Parent)\n\t\t\ttqm.mu.Unlock()\n\t\t}\n\n\t\t// Execute the task\n\t\ttqm.executeTask(ctx, task)\n\t}\n\n\tlog.Infof(\"task queue consumption completed\")\n}\n\n// executeTask executes a single task\nfunc (tqm *TaskQueueManager) executeTask(ctx context.Context, task *QueuedTask) {\n\tparent := task.Parent\n\tcurrentObjs := task.Objs\n\n\t// Query index to get old state\n\tnodes, err := tqm.m.Get(ctx, parent)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to get indexed nodes for parent %s: %v\", parent, err)\n\t\treturn\n\t}\n\n\t// Calculate diff based on current index state\n\tnow := mapset.NewSet[string]()\n\tfor i := range currentObjs {\n\t\tnow.Add(currentObjs[i].GetName())\n\t}\n\told := mapset.NewSet[string]()\n\tfor i := range nodes {\n\t\told.Add(nodes[i].Name)\n\t}\n\n\ttoDelete := old.Difference(now)\n\ttoAdd := now.Difference(old)\n\n\t// Collect paths to delete\n\tvar pathsToDelete []string\n\tfor i := range nodes {\n\t\tif toDelete.Contains(nodes[i].Name) && !op.HasStorage(path.Join(parent, nodes[i].Name)) {\n\t\t\tpathsToDelete = append(pathsToDelete, path.Join(parent, nodes[i].Name))\n\t\t}\n\t}\n\n\tvar allTaskUIDs []int64\n\n\t// Execute delete first\n\tif len(pathsToDelete) > 0 {\n\t\tlog.Debugf(\"executing delete for parent %s: %d paths\", parent, len(pathsToDelete))\n\t\ttaskUIDs, err := tqm.m.batchDeleteWithTaskUID(ctx, pathsToDelete)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to batch delete for parent %s: %v\", parent, err)\n\t\t\t// Continue to add even if delete fails\n\t\t} else {\n\t\t\tallTaskUIDs = append(allTaskUIDs, taskUIDs...)\n\t\t}\n\t}\n\n\t// Collect objects to add\n\tvar nodesToAdd []model.SearchNode\n\tfor i := range currentObjs {\n\t\tif toAdd.Contains(currentObjs[i].GetName()) {\n\t\t\tlog.Debugf(\"will add index: %s\", path.Join(parent, currentObjs[i].GetName()))\n\t\t\tnodesToAdd = append(nodesToAdd, model.SearchNode{\n\t\t\t\tParent: parent,\n\t\t\t\tName:   currentObjs[i].GetName(),\n\t\t\t\tIsDir:  currentObjs[i].IsDir(),\n\t\t\t\tSize:   currentObjs[i].GetSize(),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Execute add\n\tif len(nodesToAdd) > 0 {\n\t\tlog.Debugf(\"executing add for parent %s: %d nodes\", parent, len(nodesToAdd))\n\t\ttaskUIDs, err := tqm.m.batchIndexWithTaskUID(ctx, nodesToAdd)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to batch index for parent %s: %v\", parent, err)\n\t\t} else {\n\t\t\tallTaskUIDs = append(allTaskUIDs, taskUIDs...)\n\t\t}\n\t}\n\n\t// Record all task UIDs for this parent\n\tif len(allTaskUIDs) > 0 {\n\t\ttqm.mu.Lock()\n\t\ttqm.pendingTasks[parent] = allTaskUIDs\n\t\ttqm.mu.Unlock()\n\t\tlog.Debugf(\"recorded %d taskUIDs for parent %s\", len(allTaskUIDs), parent)\n\t}\n}\n"
  },
  {
    "path": "internal/search/meilisearch/utils.go",
    "content": "package meilisearch\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\n// hashPath hashes a path with SHA-1.\n// Path-relative exact matching should use hash,\n// because filtering strings on meilisearch is case-insensitive.\nfunc hashPath(path string) string {\n\treturn utils.HashData(utils.SHA1, []byte(path))\n}\n\nfunc buildSearchDocumentFromResults(results map[string]any) *searchDocument {\n\tdocument := &searchDocument{}\n\n\t// use assertion test to avoid panic\n\tdocument.SearchNode.Parent, _ = results[\"parent\"].(string)\n\tdocument.SearchNode.Name, _ = results[\"name\"].(string)\n\tdocument.SearchNode.IsDir, _ = results[\"is_dir\"].(bool)\n\t// JSON numbers are typically float64, not int64\n\tif size, ok := results[\"size\"].(float64); ok {\n\t\tdocument.SearchNode.Size = int64(size)\n\t}\n\n\tdocument.ID, _ = results[\"id\"].(string)\n\tdocument.ParentHash, _ = results[\"parent_hash\"].(string)\n\tdocument.ParentPathHashes, _ = results[\"parent_path_hashes\"].([]string)\n\treturn document\n}\n"
  },
  {
    "path": "internal/search/search.go",
    "content": "package search\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search/searcher\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar instance searcher.Searcher = nil\n\n// Init or reset index\nfunc Init(mode string) error {\n\tif instance != nil {\n\t\t// unchanged, do nothing\n\t\tif instance.Config().Name == mode {\n\t\t\treturn nil\n\t\t}\n\t\terr := instance.Release(context.Background())\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"release instance err: %+v\", err)\n\t\t}\n\t\tinstance = nil\n\t}\n\tif Running() {\n\t\treturn fmt.Errorf(\"index is running\")\n\t}\n\tif mode == \"none\" {\n\t\tlog.Warnf(\"not enable search\")\n\t\treturn nil\n\t}\n\ts, ok := searcher.NewMap[mode]\n\tif !ok {\n\t\treturn fmt.Errorf(\"not support index: %s\", mode)\n\t}\n\ti, err := s()\n\tif err != nil {\n\t\tlog.Errorf(\"init searcher error: %+v\", err)\n\t} else {\n\t\tinstance = i\n\t}\n\treturn err\n}\n\nfunc Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {\n\treturn instance.Search(ctx, req)\n}\n\nfunc Index(ctx context.Context, parent string, obj model.Obj) error {\n\tif instance == nil {\n\t\treturn errs.SearchNotAvailable\n\t}\n\treturn instance.Index(ctx, model.SearchNode{\n\t\tParent: parent,\n\t\tName:   obj.GetName(),\n\t\tIsDir:  obj.IsDir(),\n\t\tSize:   obj.GetSize(),\n\t})\n}\n\ntype ObjWithParent struct {\n\tParent string\n\tmodel.Obj\n}\n\nfunc BatchIndex(ctx context.Context, objs []ObjWithParent) error {\n\tif instance == nil {\n\t\treturn errs.SearchNotAvailable\n\t}\n\tif len(objs) == 0 {\n\t\treturn nil\n\t}\n\tvar searchNodes []model.SearchNode\n\tfor i := range objs {\n\t\tsearchNodes = append(searchNodes, model.SearchNode{\n\t\t\tParent: objs[i].Parent,\n\t\t\tName:   objs[i].GetName(),\n\t\t\tIsDir:  objs[i].IsDir(),\n\t\t\tSize:   objs[i].GetSize(),\n\t\t})\n\t}\n\treturn instance.BatchIndex(ctx, searchNodes)\n}\n\nfunc init() {\n\top.RegisterSettingItemHook(conf.SearchIndex, func(item *model.SettingItem) error {\n\t\tlog.Debugf(\"searcher init, mode: %s\", item.Value)\n\t\treturn Init(item.Value)\n\t})\n}\n"
  },
  {
    "path": "internal/search/searcher/manage.go",
    "content": "package searcher\n\ntype New func() (Searcher, error)\n\nvar NewMap = map[string]New{}\n\nfunc RegisterSearcher(config Config, searcher New) {\n\tNewMap[config.Name] = searcher\n}\n"
  },
  {
    "path": "internal/search/searcher/searcher.go",
    "content": "package searcher\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n)\n\ntype Config struct {\n\tName       string\n\tAutoUpdate bool\n}\n\ntype Searcher interface {\n\t// Config of the searcher\n\tConfig() Config\n\t// Search specific keywords in specific path\n\tSearch(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error)\n\t// Index obj with parent\n\tIndex(ctx context.Context, node model.SearchNode) error\n\t// BatchIndex obj with parent\n\tBatchIndex(ctx context.Context, nodes []model.SearchNode) error\n\t// Get by parent\n\tGet(ctx context.Context, parent string) ([]model.SearchNode, error)\n\t// Del with prefix\n\tDel(ctx context.Context, prefix string) error\n\t// Release resource\n\tRelease(ctx context.Context) error\n\t// Clear all index\n\tClear(ctx context.Context) error\n}\n"
  },
  {
    "path": "internal/search/util.go",
    "content": "package search\n\nimport (\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/openlist\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc Progress() (*model.IndexProgress, error) {\n\tp := setting.GetStr(conf.IndexProgress)\n\tvar progress model.IndexProgress\n\terr := utils.Json.UnmarshalFromString(p, &progress)\n\treturn &progress, err\n}\n\nfunc WriteProgress(progress *model.IndexProgress) {\n\tp, err := utils.Json.MarshalToString(progress)\n\tif err != nil {\n\t\tlog.Errorf(\"marshal progress error: %+v\", err)\n\t}\n\terr = op.SaveSettingItem(&model.SettingItem{\n\t\tKey:   conf.IndexProgress,\n\t\tValue: p,\n\t\tType:  conf.TypeText,\n\t\tGroup: model.SINGLE,\n\t\tFlag:  model.PRIVATE,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"save progress error: %+v\", err)\n\t}\n}\n\nfunc updateIgnorePaths(customIgnorePaths string) {\n\tstorages := op.GetAllStorages()\n\tignorePaths := make([]string, 0)\n\tvar skipDrivers = []string{\"OpenList\", \"Virtual\"}\n\tv3Visited := make(map[string]bool)\n\tfor _, storage := range storages {\n\t\tif utils.SliceContains(skipDrivers, storage.Config().Name) {\n\t\t\tif storage.Config().Name == \"OpenList\" {\n\t\t\t\taddition := storage.GetAddition().(*openlist.Addition)\n\t\t\t\tallowIndexed, visited := v3Visited[addition.Address]\n\t\t\t\tif !visited {\n\t\t\t\t\turl := addition.Address + \"/api/public/settings\"\n\t\t\t\t\tres, err := base.RestyClient.R().Get(url)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tlog.Debugf(\"allow_indexed body: %+v\", res.String())\n\t\t\t\t\t\tallowIndexed = utils.Json.Get(res.Body(), \"data\", conf.AllowIndexed).ToString() == \"true\"\n\t\t\t\t\t\tv3Visited[addition.Address] = allowIndexed\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlog.Debugf(\"%s allow_indexed: %v\", addition.Address, allowIndexed)\n\t\t\t\tif !allowIndexed {\n\t\t\t\t\tignorePaths = append(ignorePaths, storage.GetStorage().MountPath)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tignorePaths = append(ignorePaths, storage.GetStorage().MountPath)\n\t\t\t}\n\t\t}\n\t}\n\tif customIgnorePaths != \"\" {\n\t\tignorePaths = append(ignorePaths, strings.Split(customIgnorePaths, \"\\n\")...)\n\t}\n\tconf.SlicesMap[conf.IgnorePaths] = ignorePaths\n}\n\nfunc isIgnorePath(path string) bool {\n\tfor _, ignorePath := range conf.SlicesMap[conf.IgnorePaths] {\n\t\tif strings.HasPrefix(path, ignorePath) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc init() {\n\top.RegisterSettingItemHook(conf.IgnorePaths, func(item *model.SettingItem) error {\n\t\tupdateIgnorePaths(item.Value)\n\t\treturn nil\n\t})\n\top.RegisterStorageHook(func(typ string, storage driver.Driver) {\n\t\tvar skipDrivers = []string{\"OpenList\", \"Virtual\"}\n\t\tif utils.SliceContains(skipDrivers, storage.Config().Name) {\n\t\t\tupdateIgnorePaths(setting.GetStr(conf.IgnorePaths))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/setting/setting.go",
    "content": "package setting\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\nfunc GetStr(key string, defaultValue ...string) string {\n\tval, _ := op.GetSettingItemByKey(key)\n\tif val == nil {\n\t\tif len(defaultValue) > 0 {\n\t\t\treturn defaultValue[0]\n\t\t}\n\t\treturn \"\"\n\t}\n\treturn val.Value\n}\n\nfunc GetInt(key string, defaultVal int) int {\n\ti, err := strconv.Atoi(GetStr(key))\n\tif err != nil {\n\t\treturn defaultVal\n\t}\n\treturn i\n}\n\nfunc GetBool(key string) bool {\n\treturn GetStr(key) == \"true\" || GetStr(key) == \"1\"\n}\n\nfunc GetFloat(key string, defaultVal float64) float64 {\n\tf, err := strconv.ParseFloat(GetStr(key), 64)\n\tif err != nil {\n\t\treturn defaultVal\n\t}\n\treturn f\n}\n"
  },
  {
    "path": "internal/sharing/archive.go",
    "content": "package sharing\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc archiveMeta(ctx context.Context, sid, path string, args model.SharingArchiveMetaArgs) (*model.Sharing, *model.ArchiveMetaProvider, error) {\n\tsharing, err := op.GetSharingById(sid, args.Refresh)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithStack(errs.SharingNotFound)\n\t}\n\tif !sharing.Valid() {\n\t\treturn sharing, nil, errors.WithStack(errs.InvalidSharing)\n\t}\n\tif !sharing.Verify(args.Pwd) {\n\t\treturn sharing, nil, errors.WithStack(errs.WrongShareCode)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif len(sharing.Files) == 1 || path != \"/\" {\n\t\tunwrapPath, err := op.GetSharingUnwrapPath(sharing, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing unwrap path\")\n\t\t}\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing file\")\n\t\t}\n\t\tobj, err := op.GetArchiveMeta(ctx, storage, actualPath, args.ArchiveMetaArgs)\n\t\treturn sharing, obj, err\n\t}\n\treturn nil, nil, errors.New(\"cannot get sharing root archive meta\")\n}\n\nfunc archiveList(ctx context.Context, sid, path string, args model.SharingArchiveListArgs) (*model.Sharing, []model.Obj, error) {\n\tsharing, err := op.GetSharingById(sid, args.Refresh)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithStack(errs.SharingNotFound)\n\t}\n\tif !sharing.Valid() {\n\t\treturn sharing, nil, errors.WithStack(errs.InvalidSharing)\n\t}\n\tif !sharing.Verify(args.Pwd) {\n\t\treturn sharing, nil, errors.WithStack(errs.WrongShareCode)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif len(sharing.Files) == 1 || path != \"/\" {\n\t\tunwrapPath, err := op.GetSharingUnwrapPath(sharing, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing unwrap path\")\n\t\t}\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing file\")\n\t\t}\n\t\tobj, err := op.ListArchive(ctx, storage, actualPath, args.ArchiveListArgs)\n\t\treturn sharing, obj, err\n\t}\n\treturn nil, nil, errors.New(\"cannot get sharing root archive list\")\n}\n"
  },
  {
    "path": "internal/sharing/get.go",
    "content": "package sharing\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc get(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, model.Obj, error) {\n\tsharing, err := op.GetSharingById(sid, args.Refresh)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithStack(errs.SharingNotFound)\n\t}\n\tif !sharing.Valid() {\n\t\treturn sharing, nil, errors.WithStack(errs.InvalidSharing)\n\t}\n\tif !sharing.Verify(args.Pwd) {\n\t\treturn sharing, nil, errors.WithStack(errs.WrongShareCode)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif len(sharing.Files) == 1 || path != \"/\" {\n\t\tunwrapPath, err := op.GetSharingUnwrapPath(sharing, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing unwrap path\")\n\t\t}\n\t\tif unwrapPath != \"/\" {\n\t\t\tvirtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(unwrapPath))\n\t\t\tfor _, f := range virtualFiles {\n\t\t\t\tif f.GetName() == stdpath.Base(unwrapPath) {\n\t\t\t\t\treturn sharing, f, nil\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn sharing, &model.Object{\n\t\t\t\tName:     sid,\n\t\t\t\tSize:     0,\n\t\t\t\tModified: time.Time{},\n\t\t\t\tIsFolder: true,\n\t\t\t}, nil\n\t\t}\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing file\")\n\t\t}\n\t\tobj, err := op.Get(ctx, storage, actualPath)\n\t\treturn sharing, obj, err\n\t}\n\treturn sharing, &model.Object{\n\t\tName:     sid,\n\t\tSize:     0,\n\t\tModified: time.Time{},\n\t\tIsFolder: true,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/sharing/link.go",
    "content": "package sharing\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc link(ctx context.Context, sid, path string, args *LinkArgs) (*model.Sharing, *model.Link, model.Obj, error) {\n\tsharing, err := op.GetSharingById(sid, args.SharingListArgs.Refresh)\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.WithStack(errs.SharingNotFound)\n\t}\n\tif !sharing.Valid() {\n\t\treturn sharing, nil, nil, errors.WithStack(errs.InvalidSharing)\n\t}\n\tif !sharing.Verify(args.Pwd) {\n\t\treturn sharing, nil, nil, errors.WithStack(errs.WrongShareCode)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif len(sharing.Files) == 1 || path != \"/\" {\n\t\tunwrapPath, err := op.GetSharingUnwrapPath(sharing, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.WithMessage(err, \"failed get sharing unwrap path\")\n\t\t}\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.WithMessage(err, \"failed get sharing link\")\n\t\t}\n\t\tl, obj, err := op.Link(ctx, storage, actualPath, args.LinkArgs)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.WithMessage(err, \"failed get sharing link\")\n\t\t}\n\t\tif l.URL != \"\" && !strings.HasPrefix(l.URL, \"http://\") && !strings.HasPrefix(l.URL, \"https://\") {\n\t\t\tl.URL = common.GetApiUrl(ctx) + l.URL\n\t\t}\n\t\treturn sharing, l, obj, nil\n\t}\n\treturn nil, nil, nil, errors.New(\"cannot get sharing root link\")\n}\n"
  },
  {
    "path": "internal/sharing/list.go",
    "content": "package sharing\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc list(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, []model.Obj, error) {\n\tsharing, err := op.GetSharingById(sid, args.Refresh)\n\tif err != nil {\n\t\treturn nil, nil, errors.WithStack(errs.SharingNotFound)\n\t}\n\tif !sharing.Valid() {\n\t\treturn sharing, nil, errors.WithStack(errs.InvalidSharing)\n\t}\n\tif !sharing.Verify(args.Pwd) {\n\t\treturn sharing, nil, errors.WithStack(errs.WrongShareCode)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tif len(sharing.Files) == 1 || path != \"/\" {\n\t\tunwrapPath, err := op.GetSharingUnwrapPath(sharing, path)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed get sharing unwrap path\")\n\t\t}\n\t\tvirtualFiles := op.GetStorageVirtualFilesByPath(unwrapPath)\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\t\tif err != nil && len(virtualFiles) == 0 {\n\t\t\treturn nil, nil, errors.WithMessage(err, \"failed list sharing\")\n\t\t}\n\t\tvar objs []model.Obj\n\t\tif storage != nil {\n\t\t\tobjs, err = op.List(ctx, storage, actualPath, model.ListArgs{\n\t\t\t\tRefresh: args.Refresh,\n\t\t\t\tReqPath: stdpath.Join(sid, path),\n\t\t\t})\n\t\t\tif err != nil && len(virtualFiles) == 0 {\n\t\t\t\treturn nil, nil, errors.WithMessage(err, \"failed list sharing\")\n\t\t\t}\n\t\t}\n\t\tom := model.NewObjMerge()\n\t\tobjs = om.Merge(objs, virtualFiles...)\n\t\tmodel.SortFiles(objs, sharing.OrderBy, sharing.OrderDirection)\n\t\tmodel.ExtractFolder(objs, sharing.ExtractFolder)\n\t\treturn sharing, objs, nil\n\t}\n\tobjs := make([]model.Obj, 0, len(sharing.Files))\n\tfor _, f := range sharing.Files {\n\t\tif f != \"/\" {\n\t\t\tisVf := false\n\t\t\tvirtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(f))\n\t\t\tfor _, vf := range virtualFiles {\n\t\t\t\tif vf.GetName() == stdpath.Base(f) {\n\t\t\t\t\tobjs = append(objs, vf)\n\t\t\t\t\tisVf = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif isVf {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\tcontinue\n\t\t}\n\t\tstorage, actualPath, err := op.GetStorageAndActualPath(f)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tobj, err := op.Get(ctx, storage, actualPath)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tobjs = append(objs, obj)\n\t}\n\tmodel.SortFiles(objs, sharing.OrderBy, sharing.OrderDirection)\n\tmodel.ExtractFolder(objs, sharing.ExtractFolder)\n\treturn sharing, objs, nil\n}\n"
  },
  {
    "path": "internal/sharing/sharing.go",
    "content": "package sharing\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc List(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, []model.Obj, error) {\n\tsharing, res, err := list(ctx, sid, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed list sharing %s/%s: %+v\", sid, path, err)\n\t\treturn nil, nil, err\n\t}\n\treturn sharing, res, nil\n}\n\nfunc Get(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, model.Obj, error) {\n\tsharing, res, err := get(ctx, sid, path, args)\n\tif err != nil {\n\t\tlog.Warnf(\"failed get sharing %s/%s: %s\", sid, path, err)\n\t\treturn nil, nil, err\n\t}\n\treturn sharing, res, nil\n}\n\nfunc ArchiveMeta(ctx context.Context, sid, path string, args model.SharingArchiveMetaArgs) (*model.Sharing, *model.ArchiveMetaProvider, error) {\n\tsharing, res, err := archiveMeta(ctx, sid, path, args)\n\tif err != nil {\n\t\tlog.Warnf(\"failed get sharing archive meta %s/%s: %s\", sid, path, err)\n\t\treturn nil, nil, err\n\t}\n\treturn sharing, res, nil\n}\n\nfunc ArchiveList(ctx context.Context, sid, path string, args model.SharingArchiveListArgs) (*model.Sharing, []model.Obj, error) {\n\tsharing, res, err := archiveList(ctx, sid, path, args)\n\tif err != nil {\n\t\tlog.Warnf(\"failed list sharing archive %s/%s: %s\", sid, path, err)\n\t\treturn nil, nil, err\n\t}\n\treturn sharing, res, nil\n}\n\ntype LinkArgs struct {\n\tmodel.SharingListArgs\n\tmodel.LinkArgs\n}\n\nfunc Link(ctx context.Context, sid, path string, args *LinkArgs) (*model.Sharing, *model.Link, model.Obj, error) {\n\tsharing, res, file, err := link(ctx, sid, path, args)\n\tif err != nil {\n\t\tlog.Errorf(\"failed get sharing link %s/%s: %+v\", sid, path, err)\n\t\treturn nil, nil, nil, err\n\t}\n\treturn sharing, res, file, nil\n}\n"
  },
  {
    "path": "internal/sign/archive.go",
    "content": "package sign\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/sign\"\n)\n\nvar onceArchive sync.Once\nvar instanceArchive sign.Sign\n\nfunc SignArchive(data string) string {\n\texpire := setting.GetInt(conf.LinkExpiration, 0)\n\tif expire == 0 {\n\t\treturn NotExpiredArchive(data)\n\t} else {\n\t\treturn WithDurationArchive(data, time.Duration(expire)*time.Hour)\n\t}\n}\n\nfunc WithDurationArchive(data string, d time.Duration) string {\n\tonceArchive.Do(InstanceArchive)\n\treturn instanceArchive.Sign(data, time.Now().Add(d).Unix())\n}\n\nfunc NotExpiredArchive(data string) string {\n\tonceArchive.Do(InstanceArchive)\n\treturn instanceArchive.Sign(data, 0)\n}\n\nfunc VerifyArchive(data string, sign string) error {\n\tonceArchive.Do(InstanceArchive)\n\treturn instanceArchive.Verify(data, sign)\n}\n\nfunc InstanceArchive() {\n\tinstanceArchive = sign.NewHMACSign([]byte(setting.GetStr(conf.Token) + \"-archive\"))\n}\n"
  },
  {
    "path": "internal/sign/sign.go",
    "content": "package sign\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/sign\"\n)\n\nvar once sync.Once\nvar instance sign.Sign\n\nfunc Sign(data string) string {\n\texpire := setting.GetInt(conf.LinkExpiration, 0)\n\tif expire == 0 {\n\t\treturn NotExpired(data)\n\t} else {\n\t\treturn WithDuration(data, time.Duration(expire)*time.Hour)\n\t}\n}\n\nfunc WithDuration(data string, d time.Duration) string {\n\tonce.Do(Instance)\n\treturn instance.Sign(data, time.Now().Add(d).Unix())\n}\n\nfunc NotExpired(data string) string {\n\tonce.Do(Instance)\n\treturn instance.Sign(data, 0)\n}\n\nfunc Verify(data string, sign string) error {\n\tonce.Do(Instance)\n\treturn instance.Verify(data, sign)\n}\n\nfunc Instance() {\n\tinstance = sign.NewHMACSign([]byte(setting.GetStr(conf.Token)))\n}\n"
  },
  {
    "path": "internal/stream/limit.go",
    "content": "package stream\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Limiter interface {\n\tLimit() rate.Limit\n\tBurst() int\n\tTokensAt(time.Time) float64\n\tTokens() float64\n\tAllow() bool\n\tAllowN(time.Time, int) bool\n\tReserve() *rate.Reservation\n\tReserveN(time.Time, int) *rate.Reservation\n\tWait(context.Context) error\n\tWaitN(context.Context, int) error\n\tSetLimit(rate.Limit)\n\tSetLimitAt(time.Time, rate.Limit)\n\tSetBurst(int)\n\tSetBurstAt(time.Time, int)\n}\n\nvar (\n\tClientDownloadLimit Limiter\n\tClientUploadLimit   Limiter\n\tServerDownloadLimit Limiter\n\tServerUploadLimit   Limiter\n)\n\ntype RateLimitReader struct {\n\tio.Reader\n\tLimiter Limiter\n\tCtx     context.Context\n}\n\nfunc (r *RateLimitReader) Read(p []byte) (n int, err error) {\n\tif err = r.Ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = r.Reader.Read(p)\n\tif err != nil {\n\t\treturn\n\t}\n\tif r.Limiter != nil {\n\t\terr = r.Limiter.WaitN(r.Ctx, n)\n\t}\n\treturn\n}\n\nfunc (r *RateLimitReader) Close() error {\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\ntype RateLimitWriter struct {\n\tio.Writer\n\tLimiter Limiter\n\tCtx     context.Context\n}\n\nfunc (w *RateLimitWriter) Write(p []byte) (n int, err error) {\n\tif err = w.Ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = w.Writer.Write(p)\n\tif err != nil {\n\t\treturn\n\t}\n\tif w.Limiter != nil {\n\t\terr = w.Limiter.WaitN(w.Ctx, n)\n\t}\n\treturn\n}\n\nfunc (w *RateLimitWriter) Close() error {\n\tif c, ok := w.Writer.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\ntype RateLimitFile struct {\n\tmodel.File\n\tLimiter Limiter\n\tCtx     context.Context\n}\n\nfunc (r *RateLimitFile) Read(p []byte) (n int, err error) {\n\tif err = r.Ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = r.File.Read(p)\n\tif err != nil {\n\t\treturn\n\t}\n\tif r.Limiter != nil {\n\t\terr = r.Limiter.WaitN(r.Ctx, n)\n\t}\n\treturn\n}\n\nfunc (r *RateLimitFile) ReadAt(p []byte, off int64) (n int, err error) {\n\tif err = r.Ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = r.File.ReadAt(p, off)\n\tif err != nil {\n\t\treturn\n\t}\n\tif r.Limiter != nil {\n\t\terr = r.Limiter.WaitN(r.Ctx, n)\n\t}\n\treturn\n}\n\nfunc (r *RateLimitFile) Close() error {\n\tif c, ok := r.File.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\ntype RateLimitRangeReaderFunc RangeReaderFunc\n\nfunc (f RateLimitRangeReaderFunc) RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\tif ServerDownloadLimit == nil {\n\t\treturn f(ctx, httpRange)\n\t}\n\trc, err := f(ctx, httpRange)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &RateLimitReader{\n\t\tCtx:     ctx,\n\t\tReader:  rc,\n\t\tLimiter: ServerDownloadLimit,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/stream/stream.go",
    "content": "package stream\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/buffer\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/rclone/rclone/lib/mmap\"\n\t\"go4.org/readerutil\"\n)\n\ntype FileStream struct {\n\tCtx context.Context\n\tmodel.Obj\n\tio.Reader\n\tMimetype          string\n\tWebPutAsTask      bool\n\tForceStreamUpload bool\n\tExist             model.Obj //the file existed in the destination, we can reuse some info since we wil overwrite it\n\tutils.Closers\n\tsize      int64\n\tpeekBuff  *buffer.Reader\n\toriReader io.Reader // the original reader, used for caching\n}\n\nfunc (f *FileStream) GetSize() int64 {\n\tif f.size > 0 {\n\t\treturn f.size\n\t}\n\treturn f.Obj.GetSize()\n}\n\nfunc (f *FileStream) GetMimetype() string {\n\treturn f.Mimetype\n}\n\nfunc (f *FileStream) NeedStore() bool {\n\treturn f.WebPutAsTask\n}\n\nfunc (f *FileStream) IsForceStreamUpload() bool {\n\treturn f.ForceStreamUpload\n}\n\nfunc (f *FileStream) Close() error {\n\tif f.peekBuff != nil {\n\t\tf.peekBuff.Reset()\n\t\tf.oriReader = nil\n\t\tf.peekBuff = nil\n\t}\n\treturn f.Closers.Close()\n}\n\nfunc (f *FileStream) GetExist() model.Obj {\n\treturn f.Exist\n}\nfunc (f *FileStream) SetExist(obj model.Obj) {\n\tf.Exist = obj\n}\n\n// CacheFullAndWriter save all data into tmpFile or memory.\n// It's not thread-safe!\nfunc (f *FileStream) CacheFullAndWriter(up *model.UpdateProgress, writer io.Writer) (model.File, error) {\n\tif cache := f.GetFile(); cache != nil {\n\t\t_, err := cache.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif writer == nil {\n\t\t\treturn cache, nil\n\t\t}\n\t\treader := f.Reader\n\t\tif up != nil {\n\t\t\tcacheProgress := model.UpdateProgressWithRange(*up, 0, 50)\n\t\t\t*up = model.UpdateProgressWithRange(*up, 50, 100)\n\t\t\treader = &ReaderUpdatingProgress{\n\t\t\t\tReader: &SimpleReaderWithSize{\n\t\t\t\t\tReader: reader,\n\t\t\t\t\tSize:   f.GetSize(),\n\t\t\t\t},\n\t\t\t\tUpdateProgress: cacheProgress,\n\t\t\t}\n\t\t}\n\t\t_, err = utils.CopyWithBuffer(writer, reader)\n\t\tif err == nil {\n\t\t\t_, err = cache.Seek(0, io.SeekStart)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn cache, nil\n\t}\n\n\treader := f.Reader\n\tif f.peekBuff != nil {\n\t\tf.peekBuff.Seek(0, io.SeekStart)\n\t\tif writer != nil {\n\t\t\t_, err := utils.CopyWithBuffer(writer, f.peekBuff)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tf.peekBuff.Seek(0, io.SeekStart)\n\t\t}\n\t\treader = f.oriReader\n\t}\n\tif writer != nil {\n\t\treader = io.TeeReader(reader, writer)\n\t}\n\tif f.GetSize() < 0 {\n\t\tif f.peekBuff == nil {\n\t\t\tf.peekBuff = &buffer.Reader{}\n\t\t}\n\t\t// 检查是否有数据\n\t\tbuf := []byte{0}\n\t\tn, err := io.ReadFull(reader, buf)\n\t\tif n > 0 {\n\t\t\tf.peekBuff.Append(buf[:n])\n\t\t}\n\t\tif err == io.ErrUnexpectedEOF {\n\t\t\tf.size = f.peekBuff.Size()\n\t\t\tf.Reader = f.peekBuff\n\t\t\treturn f.peekBuff, nil\n\t\t} else if err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif conf.MaxBufferLimit-n > conf.MmapThreshold && conf.MmapThreshold > 0 {\n\t\t\tm, err := mmap.Alloc(conf.MaxBufferLimit - n)\n\t\t\tif err == nil {\n\t\t\t\tf.Add(utils.CloseFunc(func() error {\n\t\t\t\t\treturn mmap.Free(m)\n\t\t\t\t}))\n\t\t\t\tn, err = io.ReadFull(reader, m)\n\t\t\t\tif n > 0 {\n\t\t\t\t\tf.peekBuff.Append(m[:n])\n\t\t\t\t}\n\t\t\t\tif err == io.ErrUnexpectedEOF {\n\t\t\t\t\tf.size = f.peekBuff.Size()\n\t\t\t\t\tf.Reader = f.peekBuff\n\t\t\t\t\treturn f.peekBuff, nil\n\t\t\t\t} else if err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmpF, err := utils.CreateTempFile(reader, 0)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tf.Add(utils.CloseFunc(func() error {\n\t\t\treturn errors.Join(tmpF.Close(), os.RemoveAll(tmpF.Name()))\n\t\t}))\n\t\tpeekF, err := buffer.NewPeekFile(f.peekBuff, tmpF)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tf.size = peekF.Size()\n\t\tf.Reader = peekF\n\t\treturn peekF, nil\n\t}\n\n\tif up != nil {\n\t\tcacheProgress := model.UpdateProgressWithRange(*up, 0, 50)\n\t\t*up = model.UpdateProgressWithRange(*up, 50, 100)\n\t\tsize := f.GetSize()\n\t\tif f.peekBuff != nil {\n\t\t\tpeekSize := f.peekBuff.Size()\n\t\t\tcacheProgress(float64(peekSize) / float64(size) * 100)\n\t\t\tsize -= peekSize\n\t\t}\n\t\treader = &ReaderUpdatingProgress{\n\t\t\tReader: &SimpleReaderWithSize{\n\t\t\t\tReader: reader,\n\t\t\t\tSize:   size,\n\t\t\t},\n\t\t\tUpdateProgress: cacheProgress,\n\t\t}\n\t}\n\n\tif f.peekBuff != nil {\n\t\tf.oriReader = reader\n\t} else {\n\t\tf.Reader = reader\n\t}\n\treturn f.cache(f.GetSize())\n}\n\nfunc (f *FileStream) GetFile() model.File {\n\tif file, ok := f.Reader.(model.File); ok {\n\t\treturn file\n\t}\n\treturn nil\n}\n\n// 从流读取指定范围的一块数据,并且不消耗流。\n// 当读取的边界超过内部设置大小后会缓存整个流。\n// 流未缓存时线程不完全\nfunc (f *FileStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {\n\tif httpRange.Length < 0 || httpRange.Start+httpRange.Length > f.GetSize() {\n\t\thttpRange.Length = f.GetSize() - httpRange.Start\n\t}\n\tif f.GetFile() != nil {\n\t\treturn io.NewSectionReader(f.GetFile(), httpRange.Start, httpRange.Length), nil\n\t}\n\n\tcache, err := f.cache(httpRange.Start + httpRange.Length)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn io.NewSectionReader(cache, httpRange.Start, httpRange.Length), nil\n}\n\n// *旧笔记\n// 使用bytes.Buffer作为io.CopyBuffer的写入对象，CopyBuffer会调用Buffer.ReadFrom\n// 即使被写入的数据量与Buffer.Cap一致，Buffer也会扩大\n\n// 确保指定大小的数据被缓存\nfunc (f *FileStream) cache(maxCacheSize int64) (model.File, error) {\n\tif maxCacheSize > int64(conf.MaxBufferLimit) {\n\t\tsize := f.GetSize()\n\t\treader := f.Reader\n\t\tif f.peekBuff != nil {\n\t\t\tsize -= f.peekBuff.Size()\n\t\t\treader = f.oriReader\n\t\t}\n\t\ttmpF, err := utils.CreateTempFile(reader, size)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tf.Add(utils.CloseFunc(func() error {\n\t\t\treturn errors.Join(tmpF.Close(), os.RemoveAll(tmpF.Name()))\n\t\t}))\n\t\tif f.peekBuff != nil {\n\t\t\tpeekF, err := buffer.NewPeekFile(f.peekBuff, tmpF)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tf.Reader = peekF\n\t\t\treturn peekF, nil\n\t\t}\n\t\tf.Reader = tmpF\n\t\treturn tmpF, nil\n\t}\n\n\tif f.peekBuff == nil {\n\t\tf.peekBuff = &buffer.Reader{}\n\t\tf.oriReader = f.Reader\n\t\tf.Reader = io.MultiReader(f.peekBuff, f.oriReader)\n\t}\n\tbufSize := maxCacheSize - f.peekBuff.Size()\n\tif bufSize <= 0 {\n\t\treturn f.peekBuff, nil\n\t}\n\tvar buf []byte\n\tif conf.MmapThreshold > 0 && bufSize >= int64(conf.MmapThreshold) {\n\t\tm, err := mmap.Alloc(int(bufSize))\n\t\tif err == nil {\n\t\t\tf.Add(utils.CloseFunc(func() error {\n\t\t\t\treturn mmap.Free(m)\n\t\t\t}))\n\t\t\tbuf = m\n\t\t}\n\t}\n\tif buf == nil {\n\t\tbuf = make([]byte, bufSize)\n\t}\n\tn, err := io.ReadFull(f.oriReader, buf)\n\tif bufSize != int64(n) {\n\t\treturn nil, fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", bufSize, n, err)\n\t}\n\tf.peekBuff.Append(buf)\n\tif f.peekBuff.Size() >= f.GetSize() {\n\t\tf.Reader = f.peekBuff\n\t}\n\treturn f.peekBuff, nil\n}\n\nvar _ model.FileStreamer = (*SeekableStream)(nil)\nvar _ model.FileStreamer = (*FileStream)(nil)\n\ntype SeekableStream struct {\n\t*FileStream\n\t// should have one of belows to support rangeRead\n\trangeReader model.RangeReaderIF\n}\n\n// NewSeekableStream create a SeekableStream from FileStream and Link\n// if FileStream.Reader is not nil, use it directly\n// else create RangeReader from Link\nfunc NewSeekableStream(fs *FileStream, link *model.Link) (*SeekableStream, error) {\n\tif len(fs.Mimetype) == 0 {\n\t\tfs.Mimetype = utils.GetMimeType(fs.Obj.GetName())\n\t}\n\n\tif fs.Reader != nil {\n\t\tfs.Add(link)\n\t\treturn &SeekableStream{FileStream: fs}, nil\n\t}\n\n\tif link != nil {\n\t\tsize := link.ContentLength\n\t\tif size <= 0 {\n\t\t\tsize = fs.GetSize()\n\t\t}\n\t\trr, err := GetRangeReaderFromLink(size, link)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, ok := rr.(*model.FileRangeReader); ok {\n\t\t\tvar rc io.ReadCloser\n\t\t\trc, err = rr.RangeRead(fs.Ctx, http_range.Range{Length: -1})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfs.Reader = rc\n\t\t\tfs.Add(rc)\n\t\t}\n\t\tfs.size = size\n\t\tfs.Add(link)\n\t\treturn &SeekableStream{FileStream: fs, rangeReader: rr}, nil\n\t}\n\treturn nil, fmt.Errorf(\"illegal seekableStream\")\n}\n\n// 如果使用缓存或者rangeReader读取指定范围的数据，是线程安全的\n// 其他特性继承自FileStream.RangeRead\nfunc (ss *SeekableStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {\n\tif ss.GetFile() == nil && ss.rangeReader != nil {\n\t\trc, err := ss.rangeReader.RangeRead(ss.Ctx, httpRange)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tss.Add(rc)\n\t\treturn rc, nil\n\t}\n\treturn ss.FileStream.RangeRead(httpRange)\n}\n\n// only provide Reader as full stream when it's demanded. in rapid-upload, we can skip this to save memory\nfunc (ss *SeekableStream) Read(p []byte) (n int, err error) {\n\tif err := ss.generateReader(); err != nil {\n\t\treturn 0, err\n\t}\n\treturn ss.FileStream.Read(p)\n}\n\nfunc (ss *SeekableStream) generateReader() error {\n\tif ss.Reader == nil {\n\t\tif ss.rangeReader == nil {\n\t\t\treturn fmt.Errorf(\"illegal seekableStream\")\n\t\t}\n\t\trc, err := ss.rangeReader.RangeRead(ss.Ctx, http_range.Range{Length: -1})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tss.Add(rc)\n\t\tss.Reader = rc\n\t}\n\treturn nil\n}\n\nfunc (ss *SeekableStream) CacheFullAndWriter(up *model.UpdateProgress, writer io.Writer) (model.File, error) {\n\tif err := ss.generateReader(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn ss.FileStream.CacheFullAndWriter(up, writer)\n}\n\ntype ReaderWithSize interface {\n\tio.Reader\n\tGetSize() int64\n}\n\ntype SimpleReaderWithSize struct {\n\tio.Reader\n\tSize int64\n}\n\nfunc (r *SimpleReaderWithSize) GetSize() int64 {\n\treturn r.Size\n}\n\nfunc (r *SimpleReaderWithSize) Close() error {\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\ntype ReaderUpdatingProgress struct {\n\tReader ReaderWithSize\n\tmodel.UpdateProgress\n\toffset int\n}\n\nfunc (r *ReaderUpdatingProgress) Read(p []byte) (n int, err error) {\n\tn, err = r.Reader.Read(p)\n\tr.offset += n\n\tr.UpdateProgress(math.Min(100.0, float64(r.offset)/float64(r.Reader.GetSize())*100.0))\n\treturn n, err\n}\n\nfunc (r *ReaderUpdatingProgress) Close() error {\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\ntype RangeReadReadAtSeeker struct {\n\tss        *SeekableStream\n\tmasterOff int64\n\treaderMap sync.Map\n\theadCache *headCache\n}\n\ntype headCache struct {\n\treader io.Reader\n\tbufs   [][]byte\n}\n\nfunc (c *headCache) head(p []byte) (int, error) {\n\tn := 0\n\tfor _, buf := range c.bufs {\n\t\tn += copy(p[n:], buf)\n\t\tif n == len(p) {\n\t\t\treturn n, nil\n\t\t}\n\t}\n\tnn, err := io.ReadFull(c.reader, p[n:])\n\tif nn > 0 {\n\t\tbuf := make([]byte, nn)\n\t\tcopy(buf, p[n:])\n\t\tc.bufs = append(c.bufs, buf)\n\t\tn += nn\n\t\tif err == io.ErrUnexpectedEOF {\n\t\t\terr = io.EOF\n\t\t}\n\t}\n\treturn n, err\n}\n\nfunc (r *headCache) Close() error {\n\tclear(r.bufs)\n\tr.bufs = nil\n\treturn nil\n}\n\nfunc (r *RangeReadReadAtSeeker) InitHeadCache() {\n\tif r.masterOff == 0 {\n\t\tvalue, _ := r.readerMap.LoadAndDelete(int64(0))\n\t\tr.headCache = &headCache{reader: value.(io.Reader)}\n\t\tr.ss.Closers.Add(r.headCache)\n\t}\n}\n\nfunc NewReadAtSeeker(ss *SeekableStream, offset int64, forceRange ...bool) (model.File, error) {\n\tif cache := ss.GetFile(); cache != nil {\n\t\t_, err := cache.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn cache, nil\n\t}\n\tr := &RangeReadReadAtSeeker{\n\t\tss:        ss,\n\t\tmasterOff: offset,\n\t}\n\tif offset != 0 || utils.IsBool(forceRange...) {\n\t\tif offset < 0 || offset > ss.GetSize() {\n\t\t\treturn nil, errors.New(\"offset out of range\")\n\t\t}\n\t\treader, err := r.getReaderAtOffset(offset)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.readerMap.Store(int64(offset), reader)\n\t} else {\n\t\tr.readerMap.Store(int64(offset), ss)\n\t}\n\treturn r, nil\n}\n\nfunc NewMultiReaderAt(ss []*SeekableStream) (readerutil.SizeReaderAt, error) {\n\treaders := make([]readerutil.SizeReaderAt, 0, len(ss))\n\tfor _, s := range ss {\n\t\tra, err := NewReadAtSeeker(s, 0)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treaders = append(readers, io.NewSectionReader(ra, 0, s.GetSize()))\n\t}\n\treturn readerutil.NewMultiReaderAt(readers...), nil\n}\n\nfunc (r *RangeReadReadAtSeeker) getReaderAtOffset(off int64) (io.Reader, error) {\n\tfor {\n\t\tvar cur int64 = -1\n\t\tr.readerMap.Range(func(key, value any) bool {\n\t\t\tk := key.(int64)\n\t\t\tif off == k {\n\t\t\t\tcur = k\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif off > k && off-k <= 4*utils.MB && k > cur {\n\t\t\t\tcur = k\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif cur < 0 {\n\t\t\tbreak\n\t\t}\n\t\tv, ok := r.readerMap.LoadAndDelete(int64(cur))\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\trr := v.(io.Reader)\n\t\tif off == int64(cur) {\n\t\t\t// logrus.Debugf(\"getReaderAtOffset match_%d\", off)\n\t\t\treturn rr, nil\n\t\t}\n\t\tn, _ := utils.CopyWithBufferN(io.Discard, rr, off-cur)\n\t\tcur += n\n\t\tif cur == off {\n\t\t\t// logrus.Debugf(\"getReaderAtOffset old_%d\", off)\n\t\t\treturn rr, nil\n\t\t}\n\t\tbreak\n\t}\n\n\t// logrus.Debugf(\"getReaderAtOffset new_%d\", off)\n\treader, err := r.ss.RangeRead(http_range.Range{Start: off, Length: -1})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn reader, nil\n}\n\nfunc (r *RangeReadReadAtSeeker) ReadAt(p []byte, off int64) (n int, err error) {\n\tif off < 0 || off >= r.ss.GetSize() {\n\t\treturn 0, io.EOF\n\t}\n\tif off == 0 && r.headCache != nil {\n\t\treturn r.headCache.head(p)\n\t}\n\tvar rr io.Reader\n\trr, err = r.getReaderAtOffset(off)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = io.ReadFull(rr, p)\n\tif n > 0 {\n\t\toff += int64(n)\n\t\tswitch err {\n\t\tcase nil:\n\t\t\tr.readerMap.Store(int64(off), rr)\n\t\tcase io.ErrUnexpectedEOF:\n\t\t\terr = io.EOF\n\t\t}\n\t}\n\treturn n, err\n}\n\nfunc (r *RangeReadReadAtSeeker) Seek(offset int64, whence int) (int64, error) {\n\tswitch whence {\n\tcase io.SeekStart:\n\tcase io.SeekCurrent:\n\t\toffset += r.masterOff\n\tcase io.SeekEnd:\n\t\toffset += r.ss.GetSize()\n\tdefault:\n\t\treturn 0, errors.New(\"Seek: invalid whence\")\n\t}\n\tif offset < 0 || offset > r.ss.GetSize() {\n\t\treturn 0, errors.New(\"Seek: invalid offset\")\n\t}\n\tr.masterOff = offset\n\treturn offset, nil\n}\n\nfunc (r *RangeReadReadAtSeeker) Read(p []byte) (n int, err error) {\n\tn, err = r.ReadAt(p, r.masterOff)\n\tif n > 0 {\n\t\tr.masterOff += int64(n)\n\t}\n\treturn n, err\n}\n"
  },
  {
    "path": "internal/stream/stream_test.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc TestFileStream_RangeRead(t *testing.T) {\n\ttype args struct {\n\t\thttpRange http_range.Range\n\t}\n\tbuf := []byte(\"github.com/OpenListTeam/OpenList\")\n\tf := &FileStream{\n\t\tObj: &model.Object{\n\t\t\tSize: int64(len(buf)),\n\t\t},\n\t\tReader: io.NopCloser(bytes.NewReader(buf)),\n\t}\n\ttests := []struct {\n\t\tname string\n\t\tf    *FileStream\n\t\targs args\n\t\twant func(f *FileStream, got io.Reader, err error) error\n\t}{\n\t\t{\n\t\t\tname: \"range 11-12\",\n\t\t\tf:    f,\n\t\t\targs: args{\n\t\t\t\thttpRange: http_range.Range{Start: 11, Length: 12},\n\t\t\t},\n\t\t\twant: func(f *FileStream, got io.Reader, err error) error {\n\t\t\t\tif f.GetFile() != nil {\n\t\t\t\t\treturn errors.New(\"cached\")\n\t\t\t\t}\n\t\t\t\tb, _ := io.ReadAll(got)\n\t\t\t\tif !bytes.Equal(buf[11:11+12], b) {\n\t\t\t\t\treturn fmt.Errorf(\"=%s ,want =%s\", b, buf[11:11+12])\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range 11-21\",\n\t\t\tf:    f,\n\t\t\targs: args{\n\t\t\t\thttpRange: http_range.Range{Start: 11, Length: 21},\n\t\t\t},\n\t\t\twant: func(f *FileStream, got io.Reader, err error) error {\n\t\t\t\tif f.GetFile() == nil {\n\t\t\t\t\treturn errors.New(\"not cached\")\n\t\t\t\t}\n\t\t\t\tb, _ := io.ReadAll(got)\n\t\t\t\tif !bytes.Equal(buf[11:11+21], b) {\n\t\t\t\t\treturn fmt.Errorf(\"=%s ,want =%s\", b, buf[11:11+21])\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.f.RangeRead(tt.args.httpRange)\n\t\t\tif err := tt.want(tt.f, got, err); err != nil {\n\t\t\t\tt.Errorf(\"FileStream.RangeRead() %v\", err)\n\t\t\t}\n\t\t})\n\t}\n\tif f.GetFile() == nil {\n\t\tt.Error(\"not cached\")\n\t}\n\tbuf2 := make([]byte, len(buf))\n\tif _, err := io.ReadFull(f, buf2); err != nil {\n\t\tt.Errorf(\"FileStream.Read() error = %v\", err)\n\t}\n\tif !bytes.Equal(buf, buf2) {\n\t\tt.Errorf(\"FileStream.Read() = %s, want %s\", buf2, buf)\n\t}\n}\n\nfunc TestFileStream_With_PreHash(t *testing.T) {\n\tbuf := []byte(\"github.com/OpenListTeam/OpenList\")\n\tf := &FileStream{\n\t\tObj: &model.Object{\n\t\t\tSize: int64(len(buf)),\n\t\t},\n\t\tReader: io.NopCloser(bytes.NewReader(buf)),\n\t}\n\n\tconst hashSize int64 = 20\n\treader, _ := f.RangeRead(http_range.Range{Start: 0, Length: hashSize})\n\tpreHash, _ := utils.HashReader(utils.SHA1, reader)\n\tif preHash == \"\" {\n\t\tt.Error(\"preHash is empty\")\n\t}\n\ttmpF, fullHash, _ := CacheFullAndHash(f, nil, utils.SHA1)\n\tfmt.Println(fullHash)\n\tfileFullHash, _ := utils.HashFile(utils.SHA1, tmpF)\n\tfmt.Println(fileFullHash)\n\tif fullHash != fileFullHash {\n\t\tt.Errorf(\"fullHash and fileFullHash should match: fullHash=%s fileFullHash=%s\", fullHash, fileFullHash)\n\t}\n}\n"
  },
  {
    "path": "internal/stream/util.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/pool\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/rclone/rclone/lib/mmap\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype RangeReaderFunc func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error)\n\nfunc (f RangeReaderFunc) RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\treturn f(ctx, httpRange)\n}\n\nfunc GetRangeReaderFromLink(size int64, link *model.Link) (model.RangeReaderIF, error) {\n\tif link.RangeReader != nil {\n\t\tif link.Concurrency < 1 && link.PartSize < 1 {\n\t\t\treturn link.RangeReader, nil\n\t\t}\n\t\tdown := net.NewDownloader(func(d *net.Downloader) {\n\t\t\td.Concurrency = link.Concurrency\n\t\t\td.PartSize = link.PartSize\n\t\t\td.HttpClient = net.GetRangeReaderHttpRequestFunc(link.RangeReader)\n\t\t})\n\t\trangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\treturn down.Download(ctx, &net.HttpRequestParams{\n\t\t\t\tRange: httpRange,\n\t\t\t\tSize:  size,\n\t\t\t})\n\t\t}\n\t\t// RangeReader只能在驱动限速\n\t\treturn RangeReaderFunc(rangeReader), nil\n\t}\n\n\tif len(link.URL) == 0 {\n\t\treturn nil, errors.New(\"invalid link: must have at least one of URL or RangeReader\")\n\t}\n\n\tif link.Concurrency > 0 || link.PartSize > 0 {\n\t\tdown := net.NewDownloader(func(d *net.Downloader) {\n\t\t\td.Concurrency = link.Concurrency\n\t\t\td.PartSize = link.PartSize\n\t\t\td.HttpClient = func(ctx context.Context, params *net.HttpRequestParams) (*http.Response, error) {\n\t\t\t\tif ServerDownloadLimit == nil {\n\t\t\t\t\treturn net.DefaultHttpRequestFunc(ctx, params)\n\t\t\t\t}\n\t\t\t\tresp, err := net.DefaultHttpRequestFunc(ctx, params)\n\t\t\t\tif err == nil && resp.Body != nil {\n\t\t\t\t\tresp.Body = &RateLimitReader{\n\t\t\t\t\t\tCtx:     ctx,\n\t\t\t\t\t\tReader:  resp.Body,\n\t\t\t\t\t\tLimiter: ServerDownloadLimit,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn resp, err\n\t\t\t}\n\t\t})\n\t\trangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\trequestHeader, _ := ctx.Value(conf.RequestHeaderKey).(http.Header)\n\t\t\theader := net.ProcessHeader(requestHeader, link.Header)\n\t\t\treturn down.Download(ctx, &net.HttpRequestParams{\n\t\t\t\tRange:     httpRange,\n\t\t\t\tSize:      size,\n\t\t\t\tURL:       link.URL,\n\t\t\t\tHeaderRef: header,\n\t\t\t})\n\t\t}\n\t\treturn RangeReaderFunc(rangeReader), nil\n\t}\n\n\trangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tif httpRange.Length < 0 || httpRange.Start+httpRange.Length > size {\n\t\t\thttpRange.Length = size - httpRange.Start\n\t\t}\n\t\trequestHeader, _ := ctx.Value(conf.RequestHeaderKey).(http.Header)\n\t\theader := net.ProcessHeader(requestHeader, link.Header)\n\t\theader = http_range.ApplyRangeToHttpHeader(httpRange, header)\n\n\t\tresponse, err := net.RequestHttp(ctx, \"GET\", header, link.URL)\n\t\tif err != nil {\n\t\t\tif _, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"http request failure, err:%w\", err)\n\t\t}\n\t\tif ServerDownloadLimit != nil {\n\t\t\tresponse.Body = &RateLimitReader{\n\t\t\t\tCtx:     ctx,\n\t\t\t\tReader:  response.Body,\n\t\t\t\tLimiter: ServerDownloadLimit,\n\t\t\t}\n\t\t}\n\t\tif httpRange.Start == 0 && httpRange.Length == size ||\n\t\t\tresponse.StatusCode == http.StatusPartialContent ||\n\t\t\tcheckContentRange(&response.Header, httpRange.Start) {\n\t\t\treturn response.Body, nil\n\t\t} else if response.StatusCode == http.StatusOK {\n\t\t\tlog.Warnf(\"remote http server not supporting range request, expect low perfromace!\")\n\t\t\treadCloser, err := net.GetRangedHttpReader(response.Body, httpRange.Start, httpRange.Length)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn readCloser, nil\n\t\t}\n\t\treturn response.Body, nil\n\t}\n\treturn RangeReaderFunc(rangeReader), nil\n}\n\nfunc GetRangeReaderFromMFile(size int64, file model.File) *model.FileRangeReader {\n\treturn &model.FileRangeReader{\n\t\tRangeReaderIF: RangeReaderFunc(func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\tlength := httpRange.Length\n\t\t\tif length < 0 || httpRange.Start+length > size {\n\t\t\t\tlength = size - httpRange.Start\n\t\t\t}\n\t\t\treturn &model.FileCloser{File: io.NewSectionReader(file, httpRange.Start, length)}, nil\n\t\t}),\n\t}\n}\n\n// 139 cloud does not properly return 206 http status code, add a hack here\nfunc checkContentRange(header *http.Header, offset int64) bool {\n\tstart, _, err := http_range.ParseContentRange(header.Get(\"Content-Range\"))\n\tif err != nil {\n\t\tlog.Warnf(\"exception trying to parse Content-Range, will ignore,err=%s\", err)\n\t}\n\tif start == offset {\n\t\treturn true\n\t}\n\treturn false\n}\n\ntype ReaderWithCtx struct {\n\tio.Reader\n\tCtx context.Context\n}\n\nfunc (r *ReaderWithCtx) Read(p []byte) (n int, err error) {\n\tif utils.IsCanceled(r.Ctx) {\n\t\treturn 0, r.Ctx.Err()\n\t}\n\treturn r.Reader.Read(p)\n}\n\nfunc (r *ReaderWithCtx) Close() error {\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\nfunc CacheFullAndHash(stream model.FileStreamer, up *model.UpdateProgress, hashType *utils.HashType, hashParams ...any) (model.File, string, error) {\n\th := hashType.NewFunc(hashParams...)\n\ttmpF, err := stream.CacheFullAndWriter(up, h)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\treturn tmpF, hex.EncodeToString(h.Sum(nil)), nil\n}\n\ntype StreamSectionReaderIF interface {\n\t// 线程不安全\n\tGetSectionReader(off, length int64) (io.ReadSeeker, error)\n\tFreeSectionReader(sr io.ReadSeeker)\n\t// 线程不安全\n\tDiscardSection(off int64, length int64) error\n}\n\nfunc NewStreamSectionReader(file model.FileStreamer, maxBufferSize int, up *model.UpdateProgress) (StreamSectionReaderIF, error) {\n\tif file.GetFile() != nil {\n\t\treturn &cachedSectionReader{file.GetFile()}, nil\n\t}\n\n\tmaxBufferSize = min(maxBufferSize, int(file.GetSize()))\n\tif maxBufferSize > conf.MaxBufferLimit {\n\t\tf, err := os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif f.Truncate(file.GetSize()) != nil {\n\t\t\t// fallback to full cache\n\t\t\t_, _ = f.Close(), os.Remove(f.Name())\n\t\t\tcache, err := file.CacheFullAndWriter(up, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &cachedSectionReader{cache}, nil\n\t\t}\n\n\t\tss := &fileSectionReader{file: file, temp: f}\n\t\tss.bufPool = &pool.Pool[*offsetWriterWithBase]{\n\t\t\tNew: func() *offsetWriterWithBase {\n\t\t\t\tbase := ss.tempOffset\n\t\t\t\tss.tempOffset += int64(maxBufferSize)\n\t\t\t\treturn &offsetWriterWithBase{io.NewOffsetWriter(ss.temp, base), base}\n\t\t\t},\n\t\t}\n\t\tfile.Add(utils.CloseFunc(func() error {\n\t\t\tss.bufPool.Reset()\n\t\t\treturn errors.Join(ss.temp.Close(), os.Remove(ss.temp.Name()))\n\t\t}))\n\t\treturn ss, nil\n\t}\n\n\tss := &directSectionReader{file: file}\n\tif conf.MmapThreshold > 0 && maxBufferSize >= conf.MmapThreshold {\n\t\tss.bufPool = &pool.Pool[[]byte]{\n\t\t\tNew: func() []byte {\n\t\t\t\tbuf, err := mmap.Alloc(maxBufferSize)\n\t\t\t\tif err == nil {\n\t\t\t\t\tfile.Add(utils.CloseFunc(func() error {\n\t\t\t\t\t\treturn mmap.Free(buf)\n\t\t\t\t\t}))\n\t\t\t\t} else {\n\t\t\t\t\tbuf = make([]byte, maxBufferSize)\n\t\t\t\t}\n\t\t\t\treturn buf\n\t\t\t},\n\t\t}\n\t} else {\n\t\tss.bufPool = &pool.Pool[[]byte]{\n\t\t\tNew: func() []byte {\n\t\t\t\treturn make([]byte, maxBufferSize)\n\t\t\t},\n\t\t}\n\t}\n\n\tfile.Add(utils.CloseFunc(func() error {\n\t\tss.bufPool.Reset()\n\t\treturn nil\n\t}))\n\treturn ss, nil\n}\n\ntype cachedSectionReader struct {\n\tcache io.ReaderAt\n}\n\nfunc (*cachedSectionReader) DiscardSection(off int64, length int64) error {\n\treturn nil\n}\nfunc (s *cachedSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {\n\treturn io.NewSectionReader(s.cache, off, length), nil\n}\nfunc (*cachedSectionReader) FreeSectionReader(sr io.ReadSeeker) {}\n\ntype fileSectionReader struct {\n\tfile       model.FileStreamer\n\tfileOffset int64\n\ttemp       *os.File\n\ttempOffset int64\n\tbufPool    *pool.Pool[*offsetWriterWithBase]\n}\n\ntype offsetWriterWithBase struct {\n\t*io.OffsetWriter\n\tbase int64\n}\n\n// 线程不安全\nfunc (ss *fileSectionReader) DiscardSection(off int64, length int64) error {\n\tif off != ss.fileOffset {\n\t\treturn fmt.Errorf(\"stream not cached: request offset %d != current offset %d\", off, ss.fileOffset)\n\t}\n\tn, err := utils.CopyWithBufferN(io.Discard, ss.file, length)\n\tss.fileOffset += n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to skip data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\treturn nil\n}\n\ntype fileBufferSectionReader struct {\n\tio.ReadSeeker\n\tfileBuf *offsetWriterWithBase\n}\n\n// 线程不安全\nfunc (ss *fileSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {\n\tif off != ss.fileOffset {\n\t\treturn nil, fmt.Errorf(\"stream not cached: request offset %d != current offset %d\", off, ss.fileOffset)\n\t}\n\tfileBuf := ss.bufPool.Get()\n\t_, _ = fileBuf.Seek(0, io.SeekStart)\n\tn, err := utils.CopyWithBufferN(fileBuf, ss.file, length)\n\tss.fileOffset += n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\treturn &fileBufferSectionReader{io.NewSectionReader(ss.temp, fileBuf.base, length), fileBuf}, nil\n}\n\nfunc (ss *fileSectionReader) FreeSectionReader(rs io.ReadSeeker) {\n\tif sr, ok := rs.(*fileBufferSectionReader); ok {\n\t\tss.bufPool.Put(sr.fileBuf)\n\t\tsr.fileBuf = nil\n\t\tsr.ReadSeeker = nil\n\t}\n}\n\ntype directSectionReader struct {\n\tfile       model.FileStreamer\n\tfileOffset int64\n\tbufPool    *pool.Pool[[]byte]\n}\n\n// 线程不安全\nfunc (ss *directSectionReader) DiscardSection(off int64, length int64) error {\n\tif off != ss.fileOffset {\n\t\treturn fmt.Errorf(\"stream not cached: request offset %d != current offset %d\", off, ss.fileOffset)\n\t}\n\tn, err := utils.CopyWithBufferN(io.Discard, ss.file, length)\n\tss.fileOffset += n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to skip data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\treturn nil\n}\n\ntype bufferSectionReader struct {\n\tio.ReadSeeker\n\tbuf []byte\n}\n\n// 线程不安全\nfunc (ss *directSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {\n\tif off != ss.fileOffset {\n\t\treturn nil, fmt.Errorf(\"stream not cached: request offset %d != current offset %d\", off, ss.fileOffset)\n\t}\n\ttempBuf := ss.bufPool.Get()\n\tbuf := tempBuf[:length]\n\tn, err := io.ReadFull(ss.file, buf)\n\tss.fileOffset += int64(n)\n\tif int64(n) != length {\n\t\treturn nil, fmt.Errorf(\"failed to read all data: (expect =%d, actual =%d) %w\", length, n, err)\n\t}\n\treturn &bufferSectionReader{bytes.NewReader(buf), buf}, nil\n}\nfunc (ss *directSectionReader) FreeSectionReader(rs io.ReadSeeker) {\n\tif sr, ok := rs.(*bufferSectionReader); ok {\n\t\tss.bufPool.Put(sr.buf[0:cap(sr.buf)])\n\t\tsr.buf = nil\n\t\tsr.ReadSeeker = nil\n\t}\n}\n"
  },
  {
    "path": "internal/task/base.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/tache\"\n)\n\ntype TaskExtension struct {\n\ttache.Base\n\tCreator    *model.User\n\tstartTime  *time.Time\n\tendTime    *time.Time\n\tTotalBytes int64\n\tApiUrl     string\n}\n\nfunc (t *TaskExtension) SetCtx(ctx context.Context) {\n\tif t.Creator != nil {\n\t\tctx = context.WithValue(ctx, conf.UserKey, t.Creator)\n\t}\n\tif len(t.ApiUrl) > 0 {\n\t\tctx = context.WithValue(ctx, conf.ApiUrlKey, t.ApiUrl)\n\t}\n\tt.Base.SetCtx(ctx)\n}\n\nfunc (t *TaskExtension) SetCreator(creator *model.User) {\n\tt.Creator = creator\n\tt.Persist()\n}\n\nfunc (t *TaskExtension) GetCreator() *model.User {\n\treturn t.Creator\n}\n\nfunc (t *TaskExtension) SetStartTime(startTime time.Time) {\n\tt.startTime = &startTime\n}\n\nfunc (t *TaskExtension) GetStartTime() *time.Time {\n\treturn t.startTime\n}\n\nfunc (t *TaskExtension) SetEndTime(endTime time.Time) {\n\tt.endTime = &endTime\n}\n\nfunc (t *TaskExtension) GetEndTime() *time.Time {\n\treturn t.endTime\n}\n\nfunc (t *TaskExtension) ClearEndTime() {\n\tt.endTime = nil\n}\n\nfunc (t *TaskExtension) SetTotalBytes(totalBytes int64) {\n\tt.TotalBytes = totalBytes\n}\n\nfunc (t *TaskExtension) GetTotalBytes() int64 {\n\treturn t.TotalBytes\n}\n\nfunc (t *TaskExtension) SetRetry(retry int, maxRetry int) {\n\tt.Base.SetRetry(retry, maxRetry)\n\tif retry > 0 || !conf.Conf.Tasks.AllowRetryCanceled || t.Ctx() == nil {\n\t\treturn\n\t}\n\tselect {\n\tcase <-t.Ctx().Done():\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tt.SetCtx(ctx)\n\t\tt.SetCancelFunc(cancel)\n\tdefault:\n\t}\n}\n\ntype TaskExtensionInfo interface {\n\ttache.TaskWithInfo\n\tGetCreator() *model.User\n\tGetStartTime() *time.Time\n\tGetEndTime() *time.Time\n\tGetTotalBytes() int64\n}\n"
  },
  {
    "path": "internal/task/manager.go",
    "content": "package task\n\nimport (\n\t\"github.com/OpenListTeam/tache\"\n)\n\ntype Manager[T tache.Task] interface {\n\tAdd(task T)\n\tCancel(id string)\n\tCancelAll()\n\tCancelByCondition(condition func(task T) bool)\n\tGetAll() []T\n\tGetByID(id string) (T, bool)\n\tGetByState(state ...tache.State) []T\n\tGetByCondition(condition func(task T) bool) []T\n\tRemove(id string)\n\tRemoveAll()\n\tRemoveByState(state ...tache.State)\n\tRemoveByCondition(condition func(task T) bool)\n\tRetry(id string)\n\tRetryAllFailed()\n}\n"
  },
  {
    "path": "internal/task_group/group.go",
    "content": "package task_group\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype OnCompletionFunc func(ctx context.Context, groupID string, payloads ...any)\ntype TaskGroupCoordinator struct {\n\tname string\n\tmu   sync.Mutex\n\n\tgroupPayloads map[string][]any\n\tgroupStates   map[string]groupState\n\tonCompletion  OnCompletionFunc\n}\n\ntype groupState struct {\n\tpending    int\n\thasSuccess bool\n}\n\nfunc NewTaskGroupCoordinator(name string, f OnCompletionFunc) *TaskGroupCoordinator {\n\treturn &TaskGroupCoordinator{\n\t\tname:          name,\n\t\tgroupPayloads: map[string][]any{},\n\t\tgroupStates:   map[string]groupState{},\n\t\tonCompletion:  f,\n\t}\n}\n\n// payload可为nil\nfunc (tgc *TaskGroupCoordinator) AddTask(groupID string, payload any) {\n\ttgc.mu.Lock()\n\tdefer tgc.mu.Unlock()\n\tstate := tgc.groupStates[groupID]\n\tstate.pending++\n\ttgc.groupStates[groupID] = state\n\tlogrus.Debugf(\"AddTask:%s ,count=%+v\", groupID, state)\n\tif payload == nil {\n\t\treturn\n\t}\n\ttgc.groupPayloads[groupID] = append(tgc.groupPayloads[groupID], payload)\n}\n\nfunc (tgc *TaskGroupCoordinator) AppendPayload(groupID string, payload any) {\n\tif payload == nil {\n\t\treturn\n\t}\n\ttgc.mu.Lock()\n\tdefer tgc.mu.Unlock()\n\ttgc.groupPayloads[groupID] = append(tgc.groupPayloads[groupID], payload)\n}\n\nfunc (tgc *TaskGroupCoordinator) Done(ctx context.Context, groupID string, success bool) {\n\ttgc.mu.Lock()\n\tdefer tgc.mu.Unlock()\n\tstate, ok := tgc.groupStates[groupID]\n\tif !ok || state.pending == 0 {\n\t\treturn\n\t}\n\tif success {\n\t\tstate.hasSuccess = true\n\t}\n\tlogrus.Debugf(\"Done:%s ,state=%+v\", groupID, state)\n\tif state.pending == 1 {\n\t\tpayloads := tgc.groupPayloads[groupID]\n\t\tdelete(tgc.groupStates, groupID)\n\t\tdelete(tgc.groupPayloads, groupID)\n\t\tif tgc.onCompletion != nil && state.hasSuccess {\n\t\t\tlogrus.Debugf(\"OnCompletion:%s\", groupID)\n\t\t\ttgc.mu.Unlock()\n\t\t\ttgc.onCompletion(ctx, groupID, payloads...)\n\t\t\ttgc.mu.Lock()\n\t\t}\n\t\treturn\n\t}\n\tstate.pending--\n\ttgc.groupStates[groupID] = state\n}\n"
  },
  {
    "path": "internal/task_group/transfer.go",
    "content": "package task_group\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype SrcPathToRemove string\n\n// ActualPath\ntype DstPathToHook string\n\nfunc HookAndRemove(ctx context.Context, dstPath string, payloads ...any) {\n\tdstStorage, dstActualPath, err := op.GetStorageAndActualPath(dstPath)\n\tif err != nil {\n\t\tlog.Error(errors.WithMessage(err, \"failed get dst storage\"))\n\t\treturn\n\t}\n\tdstNeedHandleHook := setting.GetBool(conf.HandleHookAfterWriting)\n\tdstHandleHookLimit := setting.GetFloat(conf.HandleHookRateLimit, .0)\n\tvar listLimiter *rate.Limiter\n\tif dstNeedHandleHook && dstHandleHookLimit > .0 {\n\t\tlistLimiter = rate.NewLimiter(rate.Limit(dstHandleHookLimit), 1)\n\t}\n\thookedPaths := make(map[string]struct{})\n\thandleHook := func(actualPath string) {\n\t\tif _, ok := hookedPaths[actualPath]; ok {\n\t\t\treturn\n\t\t}\n\t\tif listLimiter != nil {\n\t\t\t_ = listLimiter.Wait(ctx)\n\t\t}\n\t\tfiles, e := op.List(ctx, dstStorage, actualPath, model.ListArgs{SkipHook: true})\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"failed handle objs update hook: %v\", e)\n\t\t} else {\n\t\t\top.HandleObjsUpdateHook(ctx, utils.GetFullPath(dstStorage.GetStorage().MountPath, actualPath), files)\n\t\t\thookedPaths[actualPath] = struct{}{}\n\t\t}\n\t}\n\tif dstNeedHandleHook {\n\t\thandleHook(dstActualPath)\n\t}\n\tfor _, payload := range payloads {\n\t\tswitch p := payload.(type) {\n\t\tcase DstPathToHook:\n\t\t\tif dstNeedHandleHook {\n\t\t\t\thandleHook(string(p))\n\t\t\t}\n\t\tcase SrcPathToRemove:\n\t\t\tsrcStorage, srcActualPath, err := op.GetStorageAndActualPath(string(p))\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(errors.WithMessage(err, \"failed get src storage\"))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = verifyAndRemove(ctx, srcStorage, dstStorage, srcActualPath, dstActualPath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc verifyAndRemove(ctx context.Context, srcStorage, dstStorage driver.Driver, srcPath, dstPath string) error {\n\tsrcObj, err := op.GetUnwrap(ctx, srcStorage, srcPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", path.Join(srcStorage.GetStorage().MountPath, srcPath))\n\t}\n\n\tdstObjPath := path.Join(dstPath, srcObj.GetName())\n\tdstObj, err := op.GetUnwrap(ctx, dstStorage, dstObjPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get dst [%s] file\", path.Join(dstStorage.GetStorage().MountPath, dstObjPath))\n\t}\n\n\tif !dstObj.IsDir() {\n\t\terr = op.Remove(ctx, srcStorage, srcPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed remove %s: %+v\", path.Join(srcStorage.GetStorage().MountPath, srcPath), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Verify directory\n\tsrcObjs, err := op.List(ctx, srcStorage, srcPath, model.ListArgs{})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", path.Join(srcStorage.GetStorage().MountPath, srcPath))\n\t}\n\n\thasErr := false\n\tfor _, obj := range srcObjs {\n\t\tsrcSubPath := path.Join(srcPath, obj.GetName())\n\t\terr := verifyAndRemove(ctx, srcStorage, dstStorage, srcSubPath, dstObjPath)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\thasErr = true\n\t\t}\n\t}\n\tif hasErr {\n\t\treturn errors.Errorf(\"some subitems of [%s] failed to verify and remove\", path.Join(srcStorage.GetStorage().MountPath, srcPath))\n\t}\n\terr = op.Remove(ctx, srcStorage, srcPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed remove %s: %+v\", path.Join(srcStorage.GetStorage().MountPath, srcPath), err)\n\t}\n\treturn nil\n}\n\nvar TransferCoordinator *TaskGroupCoordinator = NewTaskGroupCoordinator(\"HookAndRemove\", HookAndRemove)\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport \"github.com/OpenListTeam/OpenList/v4/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/README.md",
    "content": "# PACKAGE DOCUMENTATION\n\n**package rpc**\n    \n    import \"github.com/matzoe/argo/rpc\"\n\n\n\n## FUNCTIONS\n\n```\nfunc Call(address, method string, params, reply interface{}) error\n```\n\n## TYPES\n\n```\ntype Client struct {\n    // contains filtered or unexported fields\n}\n```\n\n```\nfunc New(uri string) *Client\n```\n\n```\nfunc (id *Client) AddMetalink(uri string, options ...interface{}) (gid string, err error)\n```\n`aria2.addMetalink(metalink[, options[, position]])` This method adds Metalink download by uploading \".metalink\" file. `metalink` is of type base64 which contains Base64-encoded \".metalink\" file. `options` is of type struct and its members are a pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at `position` in the\nwaiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns array of GID of registered download. If `--rpc-save-upload-metadata` is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus \".metalink\" in the directory specified by `--dir` option. The example of filename is 0a3893293e27ac0490424c06de4d09242215f0a6.metalink. If same file already exists, it is overwritten. If the file cannot be saved successfully or `--rpc-save-upload-metadata` is false, the downloads added by this method are not saved by `--save-session`.\n\n```\nfunc (id *Client) AddTorrent(filename string, options ...interface{}) (gid string, err error)\n```\n`aria2.addTorrent(torrent[, uris[, options[, position]]])` This method adds BitTorrent download by uploading \".torrent\" file. If you want to add BitTorrent Magnet URI, use `aria2.addUri()` method instead. torrent is of type base64 which contains Base64-encoded \".torrent\" file. `uris` is of type array and its element is URI which is of type string. `uris` is used for Web-seeding. For single file torrents, URI can be a complete URI pointing to the resource or if URI ends with /, name in torrent file is added. For multi-file torrents, name and path in torrent are added to form a URI for each file. options is of type struct and its members are\na pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at `position` in the waiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns GID of registered download. If `--rpc-save-upload-metadata` is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus \".torrent\" in the\ndirectory specified by `--dir` option. The example of filename is 0a3893293e27ac0490424c06de4d09242215f0a6.torrent. If same file already exists, it is overwritten. If the file cannot be saved successfully or `--rpc-save-upload-metadata` is false, the downloads added by this method are not saved by -`-save-session`.\n\n```\nfunc (id *Client) AddUri(uri string, options ...interface{}) (gid string, err error)\n```\n\n`aria2.addUri(uris[, options[, position]])` This method adds new HTTP(S)/FTP/BitTorrent Magnet URI. `uris` is of type array and its element is URI which is of type string. For BitTorrent Magnet URI, `uris` must have only one element and it should be BitTorrent Magnet URI. URIs in uris must point to the same file. If you mix other URIs which point to another file, aria2 does not complain but download may\nfail. `options` is of type struct and its members are a pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at position in the waiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns GID of registered download.\n\n```\nfunc (id *Client) ChangeGlobalOption(options map[string]interface{}) (g string, err error)\n```\n\n`aria2.changeGlobalOption(options)` This method changes global options dynamically. `options` is of type struct. The following `options` are available:\n\n    download-result\n    log\n    log-level\n    max-concurrent-downloads\n    max-download-result\n    max-overall-download-limit\n    max-overall-upload-limit\n    save-cookies\n    save-session\n    server-stat-of\n\nIn addition to them, options listed in Input File subsection are available, except for following options: `checksum`, `index-out`, `out`, `pause` and `select-file`. Using `log` option, you can dynamically start logging or change log file. To stop logging, give empty string(\"\") as a parameter value. Note that log file is always opened in append mode. This method returns OK for success.\n\n```\nfunc (id *Client) ChangeOption(gid string, options map[string]interface{}) (g string, err error)\n```\n\n`aria2.changeOption(gid, options)` This method changes options of the download denoted by `gid` dynamically. `gid` is of type string. `options` is of type struct. The following `options` are available for active downloads:\n\n    bt-max-peers\n    bt-request-peer-speed-limit\n    bt-remove-unselected-file\n    force-save\n    max-download-limit\n    max-upload-limit\n\nFor waiting or paused downloads, in addition to the above options, options listed in Input File subsection are available, except for following options: dry-run, metalink-base-uri, parameterized-uri, pause, piece-length and rpc-save-upload-metadata option. This method returns OK for success.\n\n```\nfunc (id *Client) ChangePosition(gid string, pos int, how string) (p int, err error)\n```\n\n`aria2.changePosition(gid, pos, how)` This method changes the position of the download denoted by `gid`. `pos` is of type integer. `how` is of type string. If `how` is `POS_SET`, it moves the download to a position relative to the beginning of the queue. If `how` is `POS_CUR`, it moves the download to a position relative to the current position. If `how` is `POS_END`, it moves the download to a position relative to the end of the queue. If the destination position is less than 0 or beyond the end\nof the queue, it moves the download to the beginning or the end of the queue respectively. The response is of type integer and it is the destination position.\n\n```\nfunc (id *Client) ChangeUri(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error)\n```\n\n`aria2.changeUri(gid, fileIndex, delUris, addUris[, position])` This method removes URIs in `delUris` from and appends URIs in `addUris` to download denoted by gid. `delUris` and `addUris` are list of string. A download can contain multiple files and URIs are attached to each file. `fileIndex` is used to select which file to remove/attach given URIs. `fileIndex` is 1-based. `position` is used to specify where URIs are inserted in the existing waiting URI list. `position` is 0-based. When\n`position` is omitted, URIs are appended to the back of the list. This method first execute removal and then addition. `position` is the `position` after URIs are removed, not the `position` when this method is called. When removing URI, if same URIs exist in download, only one of them is removed for each URI in delUris. In other words, there are three URIs http://example.org/aria2 and you want remove them all, you\nhave to specify (at least) 3 http://example.org/aria2 in delUris. This method returns a list which contains 2 integers. The first integer is the number of URIs deleted. The second integer is the number of URIs added.\n\n```\nfunc (id *Client) ForcePause(gid string) (g string, err error)\n```\n\n`aria2.forcePause(pid)` This method pauses the download denoted by `gid`. This method behaves just like aria2.pause() except that this method pauses download without any action which takes time such as contacting BitTorrent tracker.\n\n```\nfunc (id *Client) ForcePauseAll() (g string, err error)\n```\n\n`aria2.forcePauseAll()` This method is equal to calling `aria2.forcePause()` for every active/waiting download. This methods returns OK for success.\n\n```\nfunc (id *Client) ForceRemove(gid string) (g string, err error)\n```\n\n`aria2.forceRemove(gid)` This method removes the download denoted by `gid`. This method behaves just like aria2.remove() except that this method removes download without any action which takes time such as contacting BitTorrent tracker.\n\n```\nfunc (id *Client) ForceShutdown() (g string, err error)\n```\n\n`aria2.forceShutdown()` This method shutdowns aria2. This method behaves like `aria2.shutdown()` except that any actions which takes time such as contacting BitTorrent tracker are skipped. This method returns OK. \n\n```\nfunc (id *Client) GetFiles(gid string) (m map[string]interface{}, err error)\n```\n\n`aria2.getFiles(gid)` This method returns file list of the download denoted by `gid`. `gid` is of type string.\n\n```\nfunc (id *Client) GetGlobalOption() (m map[string]interface{}, err error)\n```\n\n`aria2.getGlobalOption()` This method returns global options. The response is of type struct. Its key is the name of option. The value type is string. Note that this method does not return options which have no default value and have not been set by the command-line options, configuration files or RPC methods. Because global options are used as a template for the options of newly added download, the response contains\nkeys returned by `aria2.getOption()` method.\n\n```\nfunc (id *Client) GetGlobalStat() (m map[string]interface{}, err error)\n```\n\n`aria2.getGlobalStat()` This method returns global statistics such as overall download and upload speed.\n\n```\nfunc (id *Client) GetOption(gid string) (m map[string]interface{}, err error)\n```\n\n`aria2.getOption(gid)` This method returns options of the download denoted by `gid`. The response is of type struct. Its key is the name of option. The value type is string. Note that this method does not return options which have no default value and have not been set by the command-line options, configuration files or RPC methods.\n\n```\nfunc (id *Client) GetPeers(gid string) (m []map[string]interface{}, err error)\n```\n\n`aria2.getPeers(gid)` This method returns peer list of the download denoted by `gid`. `gid` is of type string. This method is for BitTorrent only.\n\n```\nfunc (id *Client) GetServers(gid string) (m []map[string]interface{}, err error)\n```\n\n`aria2.getServers(gid)` This method returns currently connected HTTP(S)/FTP servers of the download denoted by `gid`. `gid` is of type string.\n\n```\nfunc (id *Client) GetSessionInfo() (m map[string]interface{}, err error)\n```\n\n`aria2.getSessionInfo()` This method returns session information.\n\n```\nfunc (id *Client) GetUris(gid string) (m map[string]interface{}, err error)\n```\n\n`aria2.getUris(gid)` This method returns URIs used in the download denoted by `gid`. `gid` is of type string.\n\n```\nfunc (id *Client) GetVersion() (m map[string]interface{}, err error)\n```\n\n`aria2.getVersion()` This method returns version of the program and the list of enabled features.\n\n```\nfunc (id *Client) Multicall(methods []map[string]interface{}) (r []interface{}, err error)\n```\n\n`system.multicall(methods)` This method encapsulates multiple method calls in a single request. `methods` is of type array and its element is struct. The struct contains two keys: `methodName` and `params`. `methodName` is the method name to call and `params` is array containing parameters to the method. This method returns array of responses. The element of array will either be a one-item array containing the return value of each method call or struct of fault element if an encapsulated method call fails.\n\n```\nfunc (id *Client) Pause(gid string) (g string, err error)\n```\n\n`aria2.pause(gid)` This method pauses the download denoted by `gid`. `gid` is of type string. The status of paused download becomes paused. If the download is active, the download is placed on the first position of waiting queue. As long as the status is paused, the download is not started. To change status to waiting, use `aria2.unpause()` method. This method returns GID of paused download.\n\n```\nfunc (id *Client) PauseAll() (g string, err error)\n```\n\n`aria2.pauseAll()` This method is equal to calling `aria2.pause()` for every active/waiting download. This methods returns OK for success.\n\n```\nfunc (id *Client) PurgeDownloadResult() (g string, err error)\n```\n\n`aria2.purgeDownloadResult()` This method purges completed/error/removed downloads to free memory. This method returns OK.\n\n```\nfunc (id *Client) Remove(gid string) (g string, err error)\n```\n\n`aria2.remove(gid)` This method removes the download denoted by gid. `gid` is of type string. If specified download is in progress, it is stopped at first. The status of removed download becomes removed. This method returns GID of removed download.\n\n```\nfunc (id *Client) RemoveDownloadResult(gid string) (g string, err error)\n```\n\n`aria2.removeDownloadResult(gid)` This method removes completed/error/removed download denoted by `gid` from memory. This method returns OK for success.\n\n```\nfunc (id *Client) Shutdown() (g string, err error)\n```\n\n`aria2.shutdown()` This method shutdowns aria2. This method returns OK.\n\n```\nfunc (id *Client) TellActive(keys ...string) (m []map[string]interface{}, err error)\n```\n\n`aria2.tellActive([keys])` This method returns the list of active downloads. The response is of type array and its element is the same struct returned by `aria2.tellStatus()` method. For `keys` parameter, please refer to `aria2.tellStatus()` method.\n\n```\nfunc (id *Client) TellStatus(gid string, keys ...string) (m map[string]interface{}, err error)\n```\n\n`aria2.tellStatus(gid[, keys])` This method returns download progress of the download denoted by `gid`. `gid` is of type string. `keys` is array of string. If it is specified, the response contains only keys in `keys` array. If `keys` is empty or not specified, the response contains all keys. This is useful when you just want specific keys and avoid unnecessary transfers. For example, `aria2.tellStatus(\"2089b05ecca3d829\", [\"gid\", \"status\"])` returns `gid` and `status` key.\n\n```\nfunc (id *Client) TellStopped(offset, num int, keys ...string) (m []map[string]interface{}, err error)\n```\n\n`aria2.tellStopped(offset, num[, keys])` This method returns the list of stopped download. `offset` is of type integer and specifies the `offset` from the oldest download. `num` is of type integer and specifies the number of downloads to be returned. For keys parameter, please refer to `aria2.tellStatus()` method. `offset` and `num` have the same semantics as `aria2.tellWaiting()` method. The response is of type array and its element is the same struct returned by `aria2.tellStatus()` method.\n\n```\nfunc (id *Client) TellWaiting(offset, num int, keys ...string) (m []map[string]interface{}, err error)\n```\n`aria2.tellWaiting(offset, num[, keys])` This method returns the list of waiting download, including paused downloads. `offset` is of type integer and specifies the `offset` from the download waiting at the front. num is of type integer and specifies the number of downloads to be returned. For keys parameter, please refer to aria2.tellStatus() method. If `offset` is a positive integer, this method returns downloads\nin the range of `[offset, offset + num)`. `offset` can be a negative integer. `offset == -1` points last download in the waiting queue and `offset == -2` points the download before the last download, and so on. The downloads in the response are in reversed order. For example, imagine that three downloads \"A\",\"B\" and \"C\" are waiting in this order.\n\n    aria2.tellWaiting(0, 1) returns [\"A\"].\n    aria2.tellWaiting(1, 2) returns [\"B\", \"C\"].\n    aria2.tellWaiting(-1, 2) returns [\"C\", \"B\"].\n\nThe response is of type array and its element is the same struct returned by `aria2.tellStatus()` method.\n\n```\nfunc (id *Client) Unpause(gid string) (g string, err error)\n```\n\n`aria2.unpause(gid)` This method changes the status of the download denoted by `gid` from paused to waiting. This makes the download eligible to restart. `gid` is of type string. This method returns GID of unpaused download.\n\n```\nfunc (id *Client) UnpauseAll() (g string, err error)\n```\n\n`aria2.unpauseAll()` This method is equal to calling `aria2.unpause()` for every active/waiting download. This methods returns OK for success.\n"
  },
  {
    "path": "pkg/aria2/rpc/call.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype caller interface {\n\t// Call sends a request of rpc to aria2 daemon\n\tCall(method string, params, reply interface{}) (err error)\n\tClose() error\n}\n\ntype httpCaller struct {\n\turi    string\n\tc      *http.Client\n\tcancel context.CancelFunc\n\twg     *sync.WaitGroup\n\tonce   sync.Once\n}\n\nfunc newHTTPCaller(ctx context.Context, u *url.URL, timeout time.Duration, notifier Notifier) *httpCaller {\n\tc := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tMaxIdleConnsPerHost: 1,\n\t\t\tMaxConnsPerHost:     1,\n\t\t\t// TLSClientConfig:     tlsConfig,\n\t\t\tDial: (&net.Dialer{\n\t\t\t\tTimeout:   timeout,\n\t\t\t\tKeepAlive: 60 * time.Second,\n\t\t\t}).Dial,\n\t\t\tTLSHandshakeTimeout:   3 * time.Second,\n\t\t\tResponseHeaderTimeout: timeout,\n\t\t},\n\t}\n\tvar wg sync.WaitGroup\n\tctx, cancel := context.WithCancel(ctx)\n\th := &httpCaller{uri: u.String(), c: c, cancel: cancel, wg: &wg}\n\tif notifier != nil {\n\t\th.setNotifier(ctx, *u, notifier)\n\t}\n\treturn h\n}\n\nfunc (h *httpCaller) Close() (err error) {\n\th.once.Do(func() {\n\t\th.cancel()\n\t\th.wg.Wait()\n\t})\n\treturn\n}\n\nfunc (h *httpCaller) setNotifier(ctx context.Context, u url.URL, notifier Notifier) (err error) {\n\tu.Scheme = \"ws\"\n\tconn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)\n\tif err != nil {\n\t\treturn\n\t}\n\th.wg.Add(1)\n\tgo func() {\n\t\tdefer h.wg.Done()\n\t\tdefer conn.Close()\n\t\t<-ctx.Done()\n\t\tconn.SetWriteDeadline(time.Now().Add(time.Second))\n\t\tif err := conn.WriteMessage(websocket.CloseMessage,\n\t\t\twebsocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\")); err != nil {\n\t\t\tlog.Printf(\"sending websocket close message: %v\", err)\n\t\t}\n\t}()\n\th.wg.Add(1)\n\tgo func() {\n\t\tdefer h.wg.Done()\n\t\tvar request websocketResponse\n\t\tvar err error\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\tif err = conn.ReadJSON(&request); err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"conn.ReadJSON|err:%v\", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tswitch request.Method {\n\t\t\tcase \"aria2.onDownloadStart\":\n\t\t\t\tnotifier.OnDownloadStart(request.Params)\n\t\t\tcase \"aria2.onDownloadPause\":\n\t\t\t\tnotifier.OnDownloadPause(request.Params)\n\t\t\tcase \"aria2.onDownloadStop\":\n\t\t\t\tnotifier.OnDownloadStop(request.Params)\n\t\t\tcase \"aria2.onDownloadComplete\":\n\t\t\t\tnotifier.OnDownloadComplete(request.Params)\n\t\t\tcase \"aria2.onDownloadError\":\n\t\t\t\tnotifier.OnDownloadError(request.Params)\n\t\t\tcase \"aria2.onBtDownloadComplete\":\n\t\t\t\tnotifier.OnBtDownloadComplete(request.Params)\n\t\t\tdefault:\n\t\t\t\tlog.Printf(\"unexpected notification: %s\", request.Method)\n\t\t\t}\n\t\t}\n\t}()\n\treturn\n}\n\nfunc (h *httpCaller) Call(method string, params, reply interface{}) (err error) {\n\tpayload, err := EncodeClientRequest(method, params)\n\tif err != nil {\n\t\treturn\n\t}\n\tr, err := h.c.Post(h.uri, \"application/json\", payload)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = DecodeClientResponse(r.Body, &reply)\n\tr.Body.Close()\n\treturn\n}\n\ntype websocketCaller struct {\n\tconn     *websocket.Conn\n\tsendChan chan *sendRequest\n\tcancel   context.CancelFunc\n\twg       *sync.WaitGroup\n\tonce     sync.Once\n\ttimeout  time.Duration\n}\n\nfunc newWebsocketCaller(ctx context.Context, uri string, timeout time.Duration, notifier Notifier) (*websocketCaller, error) {\n\tvar header = http.Header{}\n\tconn, _, err := websocket.DefaultDialer.Dial(uri, header)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsendChan := make(chan *sendRequest, 16)\n\tvar wg sync.WaitGroup\n\tctx, cancel := context.WithCancel(ctx)\n\tw := &websocketCaller{conn: conn, wg: &wg, cancel: cancel, sendChan: sendChan, timeout: timeout}\n\tprocessor := NewResponseProcessor()\n\twg.Add(1)\n\tgo func() { // routine:recv\n\t\tdefer wg.Done()\n\t\tdefer cancel()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\tvar resp websocketResponse\n\t\t\tif err := conn.ReadJSON(&resp); err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"conn.ReadJSON|err:%v\", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif resp.Id == nil { // RPC notifications\n\t\t\t\tif notifier != nil {\n\t\t\t\t\tswitch resp.Method {\n\t\t\t\t\tcase \"aria2.onDownloadStart\":\n\t\t\t\t\t\tnotifier.OnDownloadStart(resp.Params)\n\t\t\t\t\tcase \"aria2.onDownloadPause\":\n\t\t\t\t\t\tnotifier.OnDownloadPause(resp.Params)\n\t\t\t\t\tcase \"aria2.onDownloadStop\":\n\t\t\t\t\t\tnotifier.OnDownloadStop(resp.Params)\n\t\t\t\t\tcase \"aria2.onDownloadComplete\":\n\t\t\t\t\t\tnotifier.OnDownloadComplete(resp.Params)\n\t\t\t\t\tcase \"aria2.onDownloadError\":\n\t\t\t\t\t\tnotifier.OnDownloadError(resp.Params)\n\t\t\t\t\tcase \"aria2.onBtDownloadComplete\":\n\t\t\t\t\t\tnotifier.OnBtDownloadComplete(resp.Params)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tlog.Printf(\"unexpected notification: %s\", resp.Method)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tprocessor.Process(resp.clientResponse)\n\t\t}\n\t}()\n\twg.Add(1)\n\tgo func() { // routine:send\n\t\tdefer wg.Done()\n\t\tdefer cancel()\n\t\tdefer w.conn.Close()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tif err := w.conn.WriteMessage(websocket.CloseMessage,\n\t\t\t\t\twebsocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\")); err != nil {\n\t\t\t\t\tlog.Printf(\"sending websocket close message: %v\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase req := <-sendChan:\n\t\t\t\tprocessor.Add(req.request.Id, func(resp clientResponse) error {\n\t\t\t\t\terr := resp.decode(req.reply)\n\t\t\t\t\treq.cancel()\n\t\t\t\t\treturn err\n\t\t\t\t})\n\t\t\t\tw.conn.SetWriteDeadline(time.Now().Add(timeout))\n\t\t\t\tw.conn.WriteJSON(req.request)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn w, nil\n}\n\nfunc (w *websocketCaller) Close() (err error) {\n\tw.once.Do(func() {\n\t\tw.cancel()\n\t\tw.wg.Wait()\n\t})\n\treturn\n}\n\nfunc (w *websocketCaller) Call(method string, params, reply interface{}) (err error) {\n\tctx, cancel := context.WithTimeout(context.Background(), w.timeout)\n\tdefer cancel()\n\tselect {\n\tcase w.sendChan <- &sendRequest{cancel: cancel, request: &clientRequest{\n\t\tVersion: \"2.0\",\n\t\tMethod:  method,\n\t\tParams:  params,\n\t\tId:      reqid(),\n\t}, reply: reply}:\n\n\tdefault:\n\t\treturn errors.New(\"sending channel blocking\")\n\t}\n\n\t<-ctx.Done()\n\tif err := ctx.Err(); err == context.DeadlineExceeded {\n\t\treturn err\n\t}\n\treturn\n}\n\ntype sendRequest struct {\n\tcancel  context.CancelFunc\n\trequest *clientRequest\n\treply   interface{}\n}\n\nvar reqid = func() func() uint64 {\n\tvar id = uint64(time.Now().UnixNano())\n\treturn func() uint64 {\n\t\treturn atomic.AddUint64(&id, 1)\n\t}\n}()\n"
  },
  {
    "path": "pkg/aria2/rpc/call_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestWebsocketCaller(t *testing.T) {\n\ttime.Sleep(time.Second)\n\tc, err := newWebsocketCaller(context.Background(), \"ws://localhost:6800/jsonrpc\", time.Second, &DummyNotifier{})\n\tif err != nil {\n\t\tt.Fatal(err.Error())\n\t}\n\tdefer c.Close()\n\n\tvar info VersionInfo\n\tif err := c.Call(aria2GetVersion, []interface{}{}, &info); err != nil {\n\t\tt.Error(err.Error())\n\t} else {\n\t\tprintln(info.Version)\n\t}\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n)\n\n// Option is a container for specifying Call parameters and returning results\ntype Option map[string]interface{}\n\ntype Client interface {\n\tProtocol\n\tClose() error\n}\n\ntype client struct {\n\tcaller\n\turl   *url.URL\n\ttoken string\n}\n\nvar (\n\terrInvalidParameter = errors.New(\"invalid parameter\")\n\terrNotImplemented   = errors.New(\"not implemented\")\n\terrConnTimeout      = errors.New(\"connect to aria2 daemon timeout\")\n)\n\n// New returns an instance of Client\nfunc New(ctx context.Context, uri string, token string, timeout time.Duration, notifier Notifier) (Client, error) {\n\tu, err := url.Parse(uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar caller caller\n\tswitch u.Scheme {\n\tcase \"http\", \"https\":\n\t\tcaller = newHTTPCaller(ctx, u, timeout, notifier)\n\tcase \"ws\", \"wss\":\n\t\tcaller, err = newWebsocketCaller(ctx, u.String(), timeout, notifier)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, errInvalidParameter\n\t}\n\tc := &client{caller: caller, url: u, token: token}\n\treturn c, nil\n}\n\n// `aria2.addUri([secret, ]uris[, options[, position]])`\n// This method adds a new download. uris is an array of HTTP/FTP/SFTP/BitTorrent URIs (strings) pointing to the same resource.\n// If you mix URIs pointing to different resources, then the download may fail or be corrupted without aria2 complaining.\n// When adding BitTorrent Magnet URIs, uris must have only one element and it should be BitTorrent Magnet URI.\n// options is a struct and its members are pairs of option name and value.\n// If position is given, it must be an integer starting from 0.\n// The new download will be inserted at position in the waiting queue.\n// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue.\n// This method returns the GID of the newly registered download.\nfunc (c *client) AddURI(uris []string, options ...interface{}) (gid string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, uris)\n\tif options != nil {\n\t\tparams = append(params, options...)\n\t}\n\terr = c.Call(aria2AddURI, params, &gid)\n\treturn\n}\n\n// `aria2.addTorrent([secret, ]torrent[, uris[, options[, position]]])`\n// This method adds a BitTorrent download by uploading a \".torrent\" file.\n// If you want to add a BitTorrent Magnet URI, use the aria2.addUri() method instead.\n// torrent must be a base64-encoded string containing the contents of the \".torrent\" file.\n// uris is an array of URIs (string). uris is used for Web-seeding.\n// For single file torrents, the URI can be a complete URI pointing to the resource; if URI ends with /, name in torrent file is added.\n// For multi-file torrents, name and path in torrent are added to form a URI for each file. options is a struct and its members are pairs of option name and value.\n// If position is given, it must be an integer starting from 0.\n// The new download will be inserted at position in the waiting queue.\n// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue.\n// This method returns the GID of the newly registered download.\n// If --rpc-save-upload-metadata is true, the uploaded data is saved as a file named as the hex string of SHA-1 hash of data plus \".torrent\" in the directory specified by --dir option.\n// E.g. a file name might be 0a3893293e27ac0490424c06de4d09242215f0a6.torrent.\n// If a file with the same name already exists, it is overwritten!\n// If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session.\nfunc (c *client) AddTorrent(filename string, options ...interface{}) (gid string, err error) {\n\tco, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn\n\t}\n\tfile := base64.StdEncoding.EncodeToString(co)\n\tparams := make([]interface{}, 0, 3)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, file)\n\tparams = append(params, []interface{}{})\n\tif options != nil {\n\t\tparams = append(params, options...)\n\t}\n\terr = c.Call(aria2AddTorrent, params, &gid)\n\treturn\n}\n\n// `aria2.addMetalink([secret, ]metalink[, options[, position]])`\n// This method adds a Metalink download by uploading a \".metalink\" file.\n// metalink is a base64-encoded string which contains the contents of the \".metalink\" file.\n// options is a struct and its members are pairs of option name and value.\n// If position is given, it must be an integer starting from 0.\n// The new download will be inserted at position in the waiting queue.\n// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue.\n// This method returns an array of GIDs of newly registered downloads.\n// If --rpc-save-upload-metadata is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus \".metalink\" in the directory specified by --dir option.\n// E.g. a file name might be 0a3893293e27ac0490424c06de4d09242215f0a6.metalink.\n// If a file with the same name already exists, it is overwritten!\n// If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session.\nfunc (c *client) AddMetalink(filename string, options ...interface{}) (gid []string, err error) {\n\tco, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn\n\t}\n\tfile := base64.StdEncoding.EncodeToString(co)\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, file)\n\tif options != nil {\n\t\tparams = append(params, options...)\n\t}\n\terr = c.Call(aria2AddMetalink, params, &gid)\n\treturn\n}\n\n// `aria2.remove([secret, ]gid)`\n// This method removes the download denoted by gid (string).\n// If the specified download is in progress, it is first stopped.\n// The status of the removed download becomes removed.\n// This method returns GID of removed download.\nfunc (c *client) Remove(gid string) (g string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2Remove, params, &g)\n\treturn\n}\n\n// `aria2.forceRemove([secret, ]gid)`\n// This method removes the download denoted by gid.\n// This method behaves just like aria2.remove() except that this method removes the download without performing any actions which take time, such as contacting BitTorrent trackers to unregister the download first.\nfunc (c *client) ForceRemove(gid string) (g string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2ForceRemove, params, &g)\n\treturn\n}\n\n// `aria2.pause([secret, ]gid)`\n// This method pauses the download denoted by gid (string).\n// The status of paused download becomes paused.\n// If the download was active, the download is placed in the front of waiting queue.\n// While the status is paused, the download is not started.\n// To change status to waiting, use the aria2.unpause() method.\n// This method returns GID of paused download.\nfunc (c *client) Pause(gid string) (g string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2Pause, params, &g)\n\treturn\n}\n\n// `aria2.pauseAll([secret])`\n// This method is equal to calling aria2.pause() for every active/waiting download.\n// This methods returns OK.\nfunc (c *client) PauseAll() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2PauseAll, params, &ok)\n\treturn\n}\n\n// `aria2.forcePause([secret, ]gid)`\n// This method pauses the download denoted by gid.\n// This method behaves just like aria2.pause() except that this method pauses downloads without performing any actions which take time, such as contacting BitTorrent trackers to unregister the download first.\nfunc (c *client) ForcePause(gid string) (g string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2ForcePause, params, &g)\n\treturn\n}\n\n// `aria2.forcePauseAll([secret])`\n// This method is equal to calling aria2.forcePause() for every active/waiting download.\n// This methods returns OK.\nfunc (c *client) ForcePauseAll() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2ForcePauseAll, params, &ok)\n\treturn\n}\n\n// `aria2.unpause([secret, ]gid)`\n// This method changes the status of the download denoted by gid (string) from paused to waiting, making the download eligible to be restarted.\n// This method returns the GID of the unpaused download.\nfunc (c *client) Unpause(gid string) (g string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2Unpause, params, &g)\n\treturn\n}\n\n// `aria2.unpauseAll([secret])`\n// This method is equal to calling aria2.unpause() for every active/waiting download.\n// This methods returns OK.\nfunc (c *client) UnpauseAll() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2UnpauseAll, params, &ok)\n\treturn\n}\n\n// `aria2.tellStatus([secret, ]gid[, keys])`\n// This method returns the progress of the download denoted by gid (string).\n// keys is an array of strings.\n// If specified, the response contains only keys in the keys array.\n// If keys is empty or omitted, the response contains all keys.\n// This is useful when you just want specific keys and avoid unnecessary transfers.\n// For example, aria2.tellStatus(\"2089b05ecca3d829\", [\"gid\", \"status\"]) returns the gid and status keys only.\n// The response is a struct and contains following keys. Values are strings.\n// https://aria2.github.io/manual/en/html/aria2c.html#aria2.tellStatus\nfunc (c *client) TellStatus(gid string, keys ...string) (info StatusInfo, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\tif keys != nil {\n\t\tparams = append(params, keys)\n\t}\n\terr = c.Call(aria2TellStatus, params, &info)\n\treturn\n}\n\n// `aria2.getUris([secret, ]gid)`\n// This method returns the URIs used in the download denoted by gid (string).\n// The response is an array of structs and it contains following keys. Values are string.\n//\n//\turi        URI\n//\tstatus    'used' if the URI is in use. 'waiting' if the URI is still waiting in the queue.\nfunc (c *client) GetURIs(gid string) (infos []URIInfo, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2GetURIs, params, &infos)\n\treturn\n}\n\n// `aria2.getFiles([secret, ]gid)`\n// This method returns the file list of the download denoted by gid (string).\n// The response is an array of structs which contain following keys. Values are strings.\n// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getFiles\nfunc (c *client) GetFiles(gid string) (infos []FileInfo, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2GetFiles, params, &infos)\n\treturn\n}\n\n// `aria2.getPeers([secret, ]gid)`\n// This method returns a list peers of the download denoted by gid (string).\n// This method is for BitTorrent only.\n// The response is an array of structs and contains the following keys. Values are strings.\n// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getPeers\nfunc (c *client) GetPeers(gid string) (infos []PeerInfo, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2GetPeers, params, &infos)\n\treturn\n}\n\n// `aria2.getServers([secret, ]gid)`\n// This method returns currently connected HTTP(S)/FTP/SFTP servers of the download denoted by gid (string).\n// The response is an array of structs and contains the following keys. Values are strings.\n// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getServers\nfunc (c *client) GetServers(gid string) (infos []ServerInfo, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2GetServers, params, &infos)\n\treturn\n}\n\n// `aria2.tellActive([secret][, keys])`\n// This method returns a list of active downloads.\n// The response is an array of the same structs as returned by the aria2.tellStatus() method.\n// For the keys parameter, please refer to the aria2.tellStatus() method.\nfunc (c *client) TellActive(keys ...string) (infos []StatusInfo, err error) {\n\tparams := make([]interface{}, 0, 1)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tif keys != nil {\n\t\tparams = append(params, keys)\n\t}\n\terr = c.Call(aria2TellActive, params, &infos)\n\treturn\n}\n\n// `aria2.tellWaiting([secret, ]offset, num[, keys])`\n// This method returns a list of waiting downloads, including paused ones.\n// offset is an integer and specifies the offset from the download waiting at the front.\n// num is an integer and specifies the max. number of downloads to be returned.\n// For the keys parameter, please refer to the aria2.tellStatus() method.\n// If offset is a positive integer, this method returns downloads in the range of [offset, offset + num).\n// offset can be a negative integer. offset == -1 points last download in the waiting queue and offset == -2 points the download before the last download, and so on.\n// Downloads in the response are in reversed order then.\n// For example, imagine three downloads \"A\",\"B\" and \"C\" are waiting in this order.\n// aria2.tellWaiting(0, 1) returns [\"A\"].\n// aria2.tellWaiting(1, 2) returns [\"B\", \"C\"].\n// aria2.tellWaiting(-1, 2) returns [\"C\", \"B\"].\n// The response is an array of the same structs as returned by aria2.tellStatus() method.\nfunc (c *client) TellWaiting(offset, num int, keys ...string) (infos []StatusInfo, err error) {\n\tparams := make([]interface{}, 0, 3)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, offset)\n\tparams = append(params, num)\n\tif keys != nil {\n\t\tparams = append(params, keys)\n\t}\n\terr = c.Call(aria2TellWaiting, params, &infos)\n\treturn\n}\n\n// `aria2.tellStopped([secret, ]offset, num[, keys])`\n// This method returns a list of stopped downloads.\n// offset is an integer and specifies the offset from the least recently stopped download.\n// num is an integer and specifies the max. number of downloads to be returned.\n// For the keys parameter, please refer to the aria2.tellStatus() method.\n// offset and num have the same semantics as described in the aria2.tellWaiting() method.\n// The response is an array of the same structs as returned by the aria2.tellStatus() method.\nfunc (c *client) TellStopped(offset, num int, keys ...string) (infos []StatusInfo, err error) {\n\tparams := make([]interface{}, 0, 3)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, offset)\n\tparams = append(params, num)\n\tif keys != nil {\n\t\tparams = append(params, keys)\n\t}\n\terr = c.Call(aria2TellStopped, params, &infos)\n\treturn\n}\n\n// `aria2.changePosition([secret, ]gid, pos, how)`\n// This method changes the position of the download denoted by gid in the queue.\n// pos is an integer. how is a string.\n// If how is POS_SET, it moves the download to a position relative to the beginning of the queue.\n// If how is POS_CUR, it moves the download to a position relative to the current position.\n// If how is POS_END, it moves the download to a position relative to the end of the queue.\n// If the destination position is less than 0 or beyond the end of the queue, it moves the download to the beginning or the end of the queue respectively.\n// The response is an integer denoting the resulting position.\n// For example, if GID#2089b05ecca3d829 is currently in position 3, aria2.changePosition('2089b05ecca3d829', -1, 'POS_CUR') will change its position to 2. Additionally aria2.changePosition('2089b05ecca3d829', 0, 'POS_SET') will change its position to 0 (the beginning of the queue).\nfunc (c *client) ChangePosition(gid string, pos int, how string) (p int, err error) {\n\tparams := make([]interface{}, 0, 3)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\tparams = append(params, pos)\n\tparams = append(params, how)\n\terr = c.Call(aria2ChangePosition, params, &p)\n\treturn\n}\n\n// `aria2.changeUri([secret, ]gid, fileIndex, delUris, addUris[, position])`\n// This method removes the URIs in delUris from and appends the URIs in addUris to download denoted by gid.\n// delUris and addUris are lists of strings.\n// A download can contain multiple files and URIs are attached to each file.\n// fileIndex is used to select which file to remove/attach given URIs. fileIndex is 1-based.\n// position is used to specify where URIs are inserted in the existing waiting URI list. position is 0-based.\n// When position is omitted, URIs are appended to the back of the list.\n// This method first executes the removal and then the addition.\n// position is the position after URIs are removed, not the position when this method is called.\n// When removing an URI, if the same URIs exist in download, only one of them is removed for each URI in delUris.\n// In other words, if there are three URIs http://example.org/aria2 and you want remove them all, you have to specify (at least) 3 http://example.org/aria2 in delUris.\n// This method returns a list which contains two integers.\n// The first integer is the number of URIs deleted.\n// The second integer is the number of URIs added.\nfunc (c *client) ChangeURI(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error) {\n\tparams := make([]interface{}, 0, 5)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\tparams = append(params, fileindex)\n\tparams = append(params, delUris)\n\tparams = append(params, addUris)\n\tif position != nil {\n\t\tparams = append(params, position[0])\n\t}\n\terr = c.Call(aria2ChangeURI, params, &p)\n\treturn\n}\n\n// `aria2.getOption([secret, ]gid)`\n// This method returns options of the download denoted by gid.\n// The response is a struct where keys are the names of options.\n// The values are strings.\n// Note that this method does not return options which have no default value and have not been set on the command-line, in configuration files or RPC methods.\nfunc (c *client) GetOption(gid string) (m Option, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2GetOption, params, &m)\n\treturn\n}\n\n// `aria2.changeOption([secret, ]gid, options)`\n// This method changes options of the download denoted by gid (string) dynamically. options is a struct.\n// The following options are available for active downloads:\n//\n//\tbt-max-peers\n//\tbt-request-peer-speed-limit\n//\tbt-remove-unselected-file\n//\tforce-save\n//\tmax-download-limit\n//\tmax-upload-limit\n//\n// For waiting or paused downloads, in addition to the above options, options listed in Input File subsection are available, except for following options: dry-run, metalink-base-uri, parameterized-uri, pause, piece-length and rpc-save-upload-metadata option.\n// This method returns OK for success.\nfunc (c *client) ChangeOption(gid string, option Option) (ok string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\tif option != nil {\n\t\tparams = append(params, option)\n\t}\n\terr = c.Call(aria2ChangeOption, params, &ok)\n\treturn\n}\n\n// `aria2.getGlobalOption([secret])`\n// This method returns the global options.\n// The response is a struct.\n// Its keys are the names of options.\n// Values are strings.\n// Note that this method does not return options which have no default value and have not been set on the command-line, in configuration files or RPC methods. Because global options are used as a template for the options of newly added downloads, the response contains keys returned by the aria2.getOption() method.\nfunc (c *client) GetGlobalOption() (m Option, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2GetGlobalOption, params, &m)\n\treturn\n}\n\n// `aria2.changeGlobalOption([secret, ]options)`\n// This method changes global options dynamically.\n// options is a struct.\n// The following options are available:\n//\n//\tbt-max-open-files\n//\tdownload-result\n//\tlog\n//\tlog-level\n//\tmax-concurrent-downloads\n//\tmax-download-result\n//\tmax-overall-download-limit\n//\tmax-overall-upload-limit\n//\tsave-cookies\n//\tsave-session\n//\tserver-stat-of\n//\n// In addition, options listed in the Input File subsection are available, except for following options: checksum, index-out, out, pause and select-file.\n// With the log option, you can dynamically start logging or change log file.\n// To stop logging, specify an empty string(\"\") as the parameter value.\n// Note that log file is always opened in append mode.\n// This method returns OK for success.\nfunc (c *client) ChangeGlobalOption(options Option) (ok string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, options)\n\terr = c.Call(aria2ChangeGlobalOption, params, &ok)\n\treturn\n}\n\n// `aria2.getGlobalStat([secret])`\n// This method returns global statistics such as the overall download and upload speeds.\n// The response is a struct and contains the following keys. Values are strings.\n//\n//\t\tdownloadSpeed      Overall download speed (byte/sec).\n//\t\tuploadSpeed        Overall upload speed(byte/sec).\n//\t\tnumActive          The number of active downloads.\n//\t\tnumWaiting         The number of waiting downloads.\n//\t\tnumStopped         The number of stopped downloads in the current session.\n//\t                    This value is capped by the --max-download-result option.\n//\t\tnumStoppedTotal    The number of stopped downloads in the current session and not capped by the --max-download-result option.\nfunc (c *client) GetGlobalStat() (info GlobalStatInfo, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2GetGlobalStat, params, &info)\n\treturn\n}\n\n// `aria2.purgeDownloadResult([secret])`\n// This method purges completed/error/removed downloads to free memory.\n// This method returns OK.\nfunc (c *client) PurgeDownloadResult() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2PurgeDownloadResult, params, &ok)\n\treturn\n}\n\n// `aria2.removeDownloadResult([secret, ]gid)`\n// This method removes a completed/error/removed download denoted by gid from memory.\n// This method returns OK for success.\nfunc (c *client) RemoveDownloadResult(gid string) (ok string, err error) {\n\tparams := make([]interface{}, 0, 2)\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\tparams = append(params, gid)\n\terr = c.Call(aria2RemoveDownloadResult, params, &ok)\n\treturn\n}\n\n// `aria2.getVersion([secret])`\n// This method returns the version of aria2 and the list of enabled features.\n// The response is a struct and contains following keys.\n//\n//\tversion            Version number of aria2 as a string.\n//\tenabledFeatures    List of enabled features. Each feature is given as a string.\nfunc (c *client) GetVersion() (info VersionInfo, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2GetVersion, params, &info)\n\treturn\n}\n\n// `aria2.getSessionInfo([secret])`\n// This method returns session information.\n// The response is a struct and contains following key.\n//\n//\tsessionId    Session ID, which is generated each time when aria2 is invoked.\nfunc (c *client) GetSessionInfo() (info SessionInfo, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2GetSessionInfo, params, &info)\n\treturn\n}\n\n// `aria2.shutdown([secret])`\n// This method shutdowns aria2.\n// This method returns OK.\nfunc (c *client) Shutdown() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2Shutdown, params, &ok)\n\treturn\n}\n\n// `aria2.forceShutdown([secret])`\n// This method shuts down aria2().\n// This method behaves like :func:'aria2.shutdown` without performing any actions which take time, such as contacting BitTorrent trackers to unregister downloads first.\n// This method returns OK.\nfunc (c *client) ForceShutdown() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2ForceShutdown, params, &ok)\n\treturn\n}\n\n// `aria2.saveSession([secret])`\n// This method saves the current session to a file specified by the --save-session option.\n// This method returns OK if it succeeds.\nfunc (c *client) SaveSession() (ok string, err error) {\n\tparams := []string{}\n\tif c.token != \"\" {\n\t\tparams = append(params, \"token:\"+c.token)\n\t}\n\terr = c.Call(aria2SaveSession, params, &ok)\n\treturn\n}\n\n// `system.multicall(methods)`\n// This methods encapsulates multiple method calls in a single request.\n// methods is an array of structs.\n// The structs contain two keys: methodName and params.\n// methodName is the method name to call and params is array containing parameters to the method call.\n// This method returns an array of responses.\n// The elements will be either a one-item array containing the return value of the method call or a struct of fault element if an encapsulated method call fails.\nfunc (c *client) Multicall(methods []Method) (r []interface{}, err error) {\n\tif len(methods) == 0 {\n\t\terr = errInvalidParameter\n\t\treturn\n\t}\n\terr = c.Call(aria2Multicall, []interface{}{methods}, &r)\n\treturn\n}\n\n// `system.listMethods()`\n// This method returns the all available RPC methods in an array of string.\n// Unlike other methods, this method does not require secret token.\n// This is safe because this method just returns the available method names.\nfunc (c *client) ListMethods() (methods []string, err error) {\n\terr = c.Call(aria2ListMethods, []string{}, &methods)\n\treturn\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/client_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestHTTPAll(t *testing.T) {\n\tconst targetURL = \"https://nodejs.org/dist/index.json\"\n\trpc, err := New(context.Background(), \"http://localhost:6800/jsonrpc\", \"\", time.Second, &DummyNotifier{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer rpc.Close()\n\tg, err := rpc.AddURI([]string{targetURL})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tprintln(g)\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.PauseAll(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellStatus(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetURIs(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetFiles(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetPeers(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellWaiting(0, 1); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellStopped(0, 1); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetOption(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetGlobalOption(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetGlobalStat(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetSessionInfo(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.Remove(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestWebsocketAll(t *testing.T) {\n\tconst targetURL = \"https://nodejs.org/dist/index.json\"\n\trpc, err := New(context.Background(), \"ws://localhost:6800/jsonrpc\", \"\", time.Second, &DummyNotifier{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer rpc.Close()\n\tg, err := rpc.AddURI([]string{targetURL})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tprintln(g)\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.PauseAll(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellStatus(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetURIs(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetFiles(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetPeers(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellWaiting(0, 1); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellStopped(0, 1); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetOption(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetGlobalOption(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetGlobalStat(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.GetSessionInfo(); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.Remove(g); err != nil {\n\t\tt.Error(err)\n\t}\n\tif _, err = rpc.TellActive(); err != nil {\n\t\tt.Error(err)\n\t}\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/const.go",
    "content": "package rpc\n\nconst (\n\taria2AddURI               = \"aria2.addUri\"\n\taria2AddTorrent           = \"aria2.addTorrent\"\n\taria2AddMetalink          = \"aria2.addMetalink\"\n\taria2Remove               = \"aria2.remove\"\n\taria2ForceRemove          = \"aria2.forceRemove\"\n\taria2Pause                = \"aria2.pause\"\n\taria2PauseAll             = \"aria2.pauseAll\"\n\taria2ForcePause           = \"aria2.forcePause\"\n\taria2ForcePauseAll        = \"aria2.forcePauseAll\"\n\taria2Unpause              = \"aria2.unpause\"\n\taria2UnpauseAll           = \"aria2.unpauseAll\"\n\taria2TellStatus           = \"aria2.tellStatus\"\n\taria2GetURIs              = \"aria2.getUris\"\n\taria2GetFiles             = \"aria2.getFiles\"\n\taria2GetPeers             = \"aria2.getPeers\"\n\taria2GetServers           = \"aria2.getServers\"\n\taria2TellActive           = \"aria2.tellActive\"\n\taria2TellWaiting          = \"aria2.tellWaiting\"\n\taria2TellStopped          = \"aria2.tellStopped\"\n\taria2ChangePosition       = \"aria2.changePosition\"\n\taria2ChangeURI            = \"aria2.changeUri\"\n\taria2GetOption            = \"aria2.getOption\"\n\taria2ChangeOption         = \"aria2.changeOption\"\n\taria2GetGlobalOption      = \"aria2.getGlobalOption\"\n\taria2ChangeGlobalOption   = \"aria2.changeGlobalOption\"\n\taria2GetGlobalStat        = \"aria2.getGlobalStat\"\n\taria2PurgeDownloadResult  = \"aria2.purgeDownloadResult\"\n\taria2RemoveDownloadResult = \"aria2.removeDownloadResult\"\n\taria2GetVersion           = \"aria2.getVersion\"\n\taria2GetSessionInfo       = \"aria2.getSessionInfo\"\n\taria2Shutdown             = \"aria2.shutdown\"\n\taria2ForceShutdown        = \"aria2.forceShutdown\"\n\taria2SaveSession          = \"aria2.saveSession\"\n\taria2Multicall            = \"system.multicall\"\n\taria2ListMethods          = \"system.listMethods\"\n)\n"
  },
  {
    "path": "pkg/aria2/rpc/json2.go",
    "content": "package rpc\n\n// based on \"github.com/gorilla/rpc/v2/json2\"\n\n// Copyright 2009 The Go Authors. All rights reserved.\n// Copyright 2012 The Gorilla Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n)\n\n// ----------------------------------------------------------------------------\n// Request and Response\n// ----------------------------------------------------------------------------\n\n// clientRequest represents a JSON-RPC request sent by a client.\ntype clientRequest struct {\n\t// JSON-RPC protocol.\n\tVersion string `json:\"jsonrpc\"`\n\n\t// A String containing the name of the method to be invoked.\n\tMethod string `json:\"method\"`\n\n\t// Object to pass as request parameter to the method.\n\tParams interface{} `json:\"params\"`\n\n\t// The request id. This can be of any type. It is used to match the\n\t// response with the request that it is replying to.\n\tId uint64 `json:\"id\"`\n}\n\n// clientResponse represents a JSON-RPC response returned to a client.\ntype clientResponse struct {\n\tVersion string           `json:\"jsonrpc\"`\n\tResult  *json.RawMessage `json:\"result\"`\n\tError   *json.RawMessage `json:\"error\"`\n\tId      *uint64          `json:\"id\"`\n}\n\n// EncodeClientRequest encodes parameters for a JSON-RPC client request.\nfunc EncodeClientRequest(method string, args interface{}) (*bytes.Buffer, error) {\n\tvar buf bytes.Buffer\n\tc := &clientRequest{\n\t\tVersion: \"2.0\",\n\t\tMethod:  method,\n\t\tParams:  args,\n\t\tId:      reqid(),\n\t}\n\tif err := json.NewEncoder(&buf).Encode(c); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &buf, nil\n}\n\nfunc (c clientResponse) decode(reply interface{}) error {\n\tif c.Error != nil {\n\t\tjsonErr := &Error{}\n\t\tif err := json.Unmarshal(*c.Error, jsonErr); err != nil {\n\t\t\treturn &Error{\n\t\t\t\tCode:    E_SERVER,\n\t\t\t\tMessage: string(*c.Error),\n\t\t\t}\n\t\t}\n\t\treturn jsonErr\n\t}\n\n\tif c.Result == nil {\n\t\treturn ErrNullResult\n\t}\n\n\treturn json.Unmarshal(*c.Result, reply)\n}\n\n// DecodeClientResponse decodes the response body of a client request into\n// the interface reply.\nfunc DecodeClientResponse(r io.Reader, reply interface{}) error {\n\tvar c clientResponse\n\tif err := json.NewDecoder(r).Decode(&c); err != nil {\n\t\treturn err\n\t}\n\treturn c.decode(reply)\n}\n\ntype ErrorCode int\n\nconst (\n\tE_PARSE       ErrorCode = -32700\n\tE_INVALID_REQ ErrorCode = -32600\n\tE_NO_METHOD   ErrorCode = -32601\n\tE_BAD_PARAMS  ErrorCode = -32602\n\tE_INTERNAL    ErrorCode = -32603\n\tE_SERVER      ErrorCode = -32000\n)\n\nvar ErrNullResult = errors.New(\"result is null\")\n\ntype Error struct {\n\t// A Number that indicates the error type that occurred.\n\tCode ErrorCode `json:\"code\"` /* required */\n\n\t// A String providing a short description of the error.\n\t// The message SHOULD be limited to a concise single sentence.\n\tMessage string `json:\"message\"` /* required */\n\n\t// A Primitive or Structured value that contains additional information about the error.\n\tData interface{} `json:\"data\"` /* optional */\n}\n\nfunc (e *Error) Error() string {\n\treturn e.Message\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/notification.go",
    "content": "package rpc\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Event struct {\n\tGid string `json:\"gid\"` // GID of the download\n}\n\n// The RPC server might send notifications to the client.\n// Notifications is unidirectional, therefore the client which receives the notification must not respond to it.\n// The method signature of a notification is much like a normal method request but lacks the id key\n\ntype websocketResponse struct {\n\tclientResponse\n\tMethod string  `json:\"method\"`\n\tParams []Event `json:\"params\"`\n}\n\n// Notifier handles rpc notification from aria2 server\ntype Notifier interface {\n\t// OnDownloadStart will be sent when a download is started.\n\tOnDownloadStart([]Event)\n\t// OnDownloadPause will be sent when a download is paused.\n\tOnDownloadPause([]Event)\n\t// OnDownloadStop will be sent when a download is stopped by the user.\n\tOnDownloadStop([]Event)\n\t// OnDownloadComplete will be sent when a download is complete. For BitTorrent downloads, this notification is sent when the download is complete and seeding is over.\n\tOnDownloadComplete([]Event)\n\t// OnDownloadError will be sent when a download is stopped due to an error.\n\tOnDownloadError([]Event)\n\t// OnBtDownloadComplete will be sent when a torrent download is complete but seeding is still going on.\n\tOnBtDownloadComplete([]Event)\n}\n\ntype DummyNotifier struct{}\n\nfunc (DummyNotifier) OnDownloadStart(events []Event)      { log.Printf(\"%s started.\", events) }\nfunc (DummyNotifier) OnDownloadPause(events []Event)      { log.Printf(\"%s paused.\", events) }\nfunc (DummyNotifier) OnDownloadStop(events []Event)       { log.Printf(\"%s stopped.\", events) }\nfunc (DummyNotifier) OnDownloadComplete(events []Event)   { log.Printf(\"%s completed.\", events) }\nfunc (DummyNotifier) OnDownloadError(events []Event)      { log.Printf(\"%s error.\", events) }\nfunc (DummyNotifier) OnBtDownloadComplete(events []Event) { log.Printf(\"bt %s completed.\", events) }\n"
  },
  {
    "path": "pkg/aria2/rpc/proc.go",
    "content": "package rpc\n\nimport \"sync\"\n\ntype ResponseProcFn func(resp clientResponse) error\n\ntype ResponseProcessor struct {\n\tcbs map[uint64]ResponseProcFn\n\tmu  *sync.RWMutex\n}\n\nfunc NewResponseProcessor() *ResponseProcessor {\n\treturn &ResponseProcessor{\n\t\tmake(map[uint64]ResponseProcFn),\n\t\t&sync.RWMutex{},\n\t}\n}\n\nfunc (r *ResponseProcessor) Add(id uint64, fn ResponseProcFn) {\n\tr.mu.Lock()\n\tr.cbs[id] = fn\n\tr.mu.Unlock()\n}\n\nfunc (r *ResponseProcessor) remove(id uint64) {\n\tr.mu.Lock()\n\tdelete(r.cbs, id)\n\tr.mu.Unlock()\n}\n\n// Process called by recv routine\nfunc (r *ResponseProcessor) Process(resp clientResponse) error {\n\tid := *resp.Id\n\tr.mu.RLock()\n\tfn, ok := r.cbs[id]\n\tr.mu.RUnlock()\n\tif ok && fn != nil {\n\t\tdefer r.remove(id)\n\t\treturn fn(resp)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/proto.go",
    "content": "package rpc\n\n// Protocol is a set of rpc methods that aria2 daemon supports\ntype Protocol interface {\n\tAddURI(uris []string, options ...interface{}) (gid string, err error)\n\tAddTorrent(filename string, options ...interface{}) (gid string, err error)\n\tAddMetalink(filename string, options ...interface{}) (gid []string, err error)\n\tRemove(gid string) (g string, err error)\n\tForceRemove(gid string) (g string, err error)\n\tPause(gid string) (g string, err error)\n\tPauseAll() (ok string, err error)\n\tForcePause(gid string) (g string, err error)\n\tForcePauseAll() (ok string, err error)\n\tUnpause(gid string) (g string, err error)\n\tUnpauseAll() (ok string, err error)\n\tTellStatus(gid string, keys ...string) (info StatusInfo, err error)\n\tGetURIs(gid string) (infos []URIInfo, err error)\n\tGetFiles(gid string) (infos []FileInfo, err error)\n\tGetPeers(gid string) (infos []PeerInfo, err error)\n\tGetServers(gid string) (infos []ServerInfo, err error)\n\tTellActive(keys ...string) (infos []StatusInfo, err error)\n\tTellWaiting(offset, num int, keys ...string) (infos []StatusInfo, err error)\n\tTellStopped(offset, num int, keys ...string) (infos []StatusInfo, err error)\n\tChangePosition(gid string, pos int, how string) (p int, err error)\n\tChangeURI(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error)\n\tGetOption(gid string) (m Option, err error)\n\tChangeOption(gid string, option Option) (ok string, err error)\n\tGetGlobalOption() (m Option, err error)\n\tChangeGlobalOption(options Option) (ok string, err error)\n\tGetGlobalStat() (info GlobalStatInfo, err error)\n\tPurgeDownloadResult() (ok string, err error)\n\tRemoveDownloadResult(gid string) (ok string, err error)\n\tGetVersion() (info VersionInfo, err error)\n\tGetSessionInfo() (info SessionInfo, err error)\n\tShutdown() (ok string, err error)\n\tForceShutdown() (ok string, err error)\n\tSaveSession() (ok string, err error)\n\tMulticall(methods []Method) (r []interface{}, err error)\n\tListMethods() (methods []string, err error)\n}\n"
  },
  {
    "path": "pkg/aria2/rpc/resp.go",
    "content": "//go:generate easyjson -all\n\npackage rpc\n\n// StatusInfo represents response of aria2.tellStatus\ntype StatusInfo struct {\n\tGid             string     `json:\"gid\"`             // GID of the download.\n\tStatus          string     `json:\"status\"`          // active for currently downloading/seeding downloads. waiting for downloads in the queue; download is not started. paused for paused downloads. error for downloads that were stopped because of error. complete for stopped and completed downloads. removed for the downloads removed by user.\n\tTotalLength     string     `json:\"totalLength\"`     // Total length of the download in bytes.\n\tCompletedLength string     `json:\"completedLength\"` // Completed length of the download in bytes.\n\tUploadLength    string     `json:\"uploadLength\"`    // Uploaded length of the download in bytes.\n\tBitField        string     `json:\"bitfield\"`        // Hexadecimal representation of the download progress. The highest bit corresponds to the piece at index 0. Any set bits indicate loaded pieces, while unset bits indicate not yet loaded and/or missing pieces. Any overflow bits at the end are set to zero. When the download was not started yet, this key will not be included in the response.\n\tDownloadSpeed   string     `json:\"downloadSpeed\"`   // Download speed of this download measured in bytes/sec.\n\tUploadSpeed     string     `json:\"uploadSpeed\"`     // Upload speed of this download measured in bytes/sec.\n\tInfoHash        string     `json:\"infoHash\"`        // InfoHash. BitTorrent only.\n\tNumSeeders      string     `json:\"numSeeders\"`      // The number of seeders aria2 has connected to. BitTorrent only.\n\tSeeder          string     `json:\"seeder\"`          // true if the local endpoint is a seeder. Otherwise, false. BitTorrent only.\n\tPieceLength     string     `json:\"pieceLength\"`     // Piece length in bytes.\n\tNumPieces       string     `json:\"numPieces\"`       // The number of pieces.\n\tConnections     string     `json:\"connections\"`     // The number of peers/servers aria2 has connected to.\n\tErrorCode       string     `json:\"errorCode\"`       // The code of the last error for this item, if any. The value is a string. The error codes are defined in the EXIT STATUS section. This value is only available for stopped/completed downloads.\n\tErrorMessage    string     `json:\"errorMessage\"`    // The (hopefully) human-readable error message associated to errorCode.\n\tFollowedBy      []string   `json:\"followedBy\"`      // List of GIDs which are generated as the result of this download. For example, when aria2 downloads a Metalink file, it generates downloads described in the Metalink (see the --follow-metalink option). This value is useful to track auto-generated downloads. If there are no such downloads, this key will not be included in the response.\n\tBelongsTo       string     `json:\"belongsTo\"`       // GID of a parent download. Some downloads are a part of another download. For example, if a file in a Metalink has BitTorrent resources, the downloads of \".torrent\" files are parts of that parent. If this download has no parent, this key will not be included in the response.\n\tDir             string     `json:\"dir\"`             // Directory to save files.\n\tFiles           []FileInfo `json:\"files\"`           // Returns the list of files. The elements of this list are the same structs used in aria2.getFiles() method.\n\tBitTorrent      struct {\n\t\tAnnounceList [][]string `json:\"announceList\"` // List of lists of announce URIs. If the torrent contains announce and no announce-list, announce is converted to the announce-list format.\n\t\tComment      string     `json:\"comment\"`      // The comment of the torrent. comment.utf-8 is used if available.\n\t\tCreationDate int64      `json:\"creationDate\"` // The creation time of the torrent. The value is an integer since the epoch, measured in seconds.\n\t\tMode         string     `json:\"mode\"`         // File mode of the torrent. The value is either single or multi.\n\t\tInfo         struct {\n\t\t\tName string `json:\"name\"` // name in info dictionary. name.utf-8 is used if available.\n\t\t} `json:\"info\"` // Struct which contains data from Info dictionary. It contains following keys.\n\t} `json:\"bittorrent\"` // Struct which contains information retrieved from the .torrent (file). BitTorrent only. It contains following keys.\n}\n\n// URIInfo represents an element of response of aria2.getUris\ntype URIInfo struct {\n\tURI    string `json:\"uri\"`    // URI\n\tStatus string `json:\"status\"` // 'used' if the URI is in use. 'waiting' if the URI is still waiting in the queue.\n}\n\n// FileInfo represents an element of response of aria2.getFiles\ntype FileInfo struct {\n\tIndex           string    `json:\"index\"`           // Index of the file, starting at 1, in the same order as files appear in the multi-file torrent.\n\tPath            string    `json:\"path\"`            // File path.\n\tLength          string    `json:\"length\"`          // File size in bytes.\n\tCompletedLength string    `json:\"completedLength\"` // Completed length of this file in bytes. Please note that it is possible that sum of completedLength is less than the completedLength returned by the aria2.tellStatus() method. This is because completedLength in aria2.getFiles() only includes completed pieces. On the other hand, completedLength in aria2.tellStatus() also includes partially completed pieces.\n\tSelected        string    `json:\"selected\"`        // true if this file is selected by --select-file option. If --select-file is not specified or this is single-file torrent or not a torrent download at all, this value is always true. Otherwise false.\n\tURIs            []URIInfo `json:\"uris\"`            // Returns a list of URIs for this file. The element type is the same struct used in the aria2.getUris() method.\n}\n\n// PeerInfo represents an element of response of aria2.getPeers\ntype PeerInfo struct {\n\tPeerId        string `json:\"peerId\"`        // Percent-encoded peer ID.\n\tIP            string `json:\"ip\"`            // IP address of the peer.\n\tPort          string `json:\"port\"`          // Port number of the peer.\n\tBitField      string `json:\"bitfield\"`      // Hexadecimal representation of the download progress of the peer. The highest bit corresponds to the piece at index 0. Set bits indicate the piece is available and unset bits indicate the piece is missing. Any spare bits at the end are set to zero.\n\tAmChoking     string `json:\"amChoking\"`     // true if aria2 is choking the peer. Otherwise false.\n\tPeerChoking   string `json:\"peerChoking\"`   // true if the peer is choking aria2. Otherwise false.\n\tDownloadSpeed string `json:\"downloadSpeed\"` // Download speed (byte/sec) that this client obtains from the peer.\n\tUploadSpeed   string `json:\"uploadSpeed\"`   // Upload speed(byte/sec) that this client uploads to the peer.\n\tSeeder        string `json:\"seeder\"`        // true if this peer is a seeder. Otherwise false.\n}\n\n// ServerInfo represents an element of response of aria2.getServers\ntype ServerInfo struct {\n\tIndex   string `json:\"index\"` // Index of the file, starting at 1, in the same order as files appear in the multi-file metalink.\n\tServers []struct {\n\t\tURI           string `json:\"uri\"`           // Original URI.\n\t\tCurrentURI    string `json:\"currentUri\"`    // This is the URI currently used for downloading. If redirection is involved, currentUri and uri may differ.\n\t\tDownloadSpeed string `json:\"downloadSpeed\"` // Download speed (byte/sec)\n\t} `json:\"servers\"` // A list of structs which contain the following keys.\n}\n\n// GlobalStatInfo represents response of aria2.getGlobalStat\ntype GlobalStatInfo struct {\n\tDownloadSpeed   string `json:\"downloadSpeed\"`   // Overall download speed (byte/sec).\n\tUploadSpeed     string `json:\"uploadSpeed\"`     // Overall upload speed(byte/sec).\n\tNumActive       string `json:\"numActive\"`       // The number of active downloads.\n\tNumWaiting      string `json:\"numWaiting\"`      // The number of waiting downloads.\n\tNumStopped      string `json:\"numStopped\"`      // The number of stopped downloads in the current session. This value is capped by the --max-download-result option.\n\tNumStoppedTotal string `json:\"numStoppedTotal\"` // The number of stopped downloads in the current session and not capped by the --max-download-result option.\n}\n\n// VersionInfo represents response of aria2.getVersion\ntype VersionInfo struct {\n\tVersion  string   `json:\"version\"`         // Version number of aria2 as a string.\n\tFeatures []string `json:\"enabledFeatures\"` // List of enabled features. Each feature is given as a string.\n}\n\n// SessionInfo represents response of aria2.getSessionInfo\ntype SessionInfo struct {\n\tId string `json:\"sessionId\"` // Session ID, which is generated each time when aria2 is invoked.\n}\n\n// Method is an element of parameters used in system.multicall\ntype Method struct {\n\tName   string        `json:\"methodName\"` // Method name to call\n\tParams []interface{} `json:\"params\"`     // Array containing parameters to the method call\n}\n"
  },
  {
    "path": "pkg/buffer/bytes.go",
    "content": "package buffer\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// 用于存储不复用的[]byte\ntype Reader struct {\n\tbufs   [][]byte\n\tsize   int64\n\toffset int64\n}\n\nfunc (r *Reader) Size() int64 {\n\treturn r.size\n}\n\nfunc (r *Reader) Append(buf []byte) {\n\tr.size += int64(len(buf))\n\tr.bufs = append(r.bufs, buf)\n}\n\nfunc (r *Reader) Read(p []byte) (int, error) {\n\tn, err := r.ReadAt(p, r.offset)\n\tif n > 0 {\n\t\tr.offset += int64(n)\n\t}\n\treturn n, err\n}\n\nfunc (r *Reader) ReadAt(p []byte, off int64) (int, error) {\n\tif off < 0 || off >= r.size {\n\t\treturn 0, io.EOF\n\t}\n\n\tn := 0\n\treadFrom := false\n\tfor _, buf := range r.bufs {\n\t\tif readFrom {\n\t\t\tnn := copy(p[n:], buf)\n\t\t\tn += nn\n\t\t\tif n == len(p) {\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t} else if newOff := off - int64(len(buf)); newOff >= 0 {\n\t\t\toff = newOff\n\t\t} else {\n\t\t\tnn := copy(p, buf[off:])\n\t\t\tif nn == len(p) {\n\t\t\t\treturn nn, nil\n\t\t\t}\n\t\t\tn += nn\n\t\t\treadFrom = true\n\t\t}\n\t}\n\n\treturn n, io.EOF\n}\n\nfunc (r *Reader) Seek(offset int64, whence int) (int64, error) {\n\tswitch whence {\n\tcase io.SeekStart:\n\tcase io.SeekCurrent:\n\t\toffset = r.offset + offset\n\tcase io.SeekEnd:\n\t\toffset = r.size + offset\n\tdefault:\n\t\treturn 0, errors.New(\"Seek: invalid whence\")\n\t}\n\n\tif offset < 0 || offset > r.size {\n\t\treturn 0, errors.New(\"Seek: invalid offset\")\n\t}\n\n\tr.offset = offset\n\treturn offset, nil\n}\n\nfunc (r *Reader) Reset() {\n\tclear(r.bufs)\n\tr.bufs = nil\n\tr.size = 0\n\tr.offset = 0\n}\n\nfunc NewReader(buf ...[]byte) *Reader {\n\tb := &Reader{\n\t\tbufs: make([][]byte, 0, len(buf)),\n\t}\n\tfor _, b1 := range buf {\n\t\tb.Append(b1)\n\t}\n\treturn b\n}\n"
  },
  {
    "path": "pkg/buffer/bytes_test.go",
    "content": "package buffer\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n)\n\nfunc TestReader_ReadAt(t *testing.T) {\n\ttype args struct {\n\t\tp   []byte\n\t\toff int64\n\t}\n\tbs := &Reader{}\n\tbs.Append([]byte(\"github.com\"))\n\tbs.Append([]byte(\"/OpenList\"))\n\tbs.Append([]byte(\"Team/\"))\n\tbs.Append([]byte(\"OpenList\"))\n\ttests := []struct {\n\t\tname string\n\t\tb    *Reader\n\t\targs args\n\t\twant func(a args, n int, err error) error\n\t}{\n\t\t{\n\t\t\tname: \"readAt len 10 offset 0\",\n\t\t\tb:    bs,\n\t\t\targs: args{\n\t\t\t\tp:   make([]byte, 10),\n\t\t\t\toff: 0,\n\t\t\t},\n\t\t\twant: func(a args, n int, err error) error {\n\t\t\t\tif n != len(a.p) {\n\t\t\t\t\treturn errors.New(\"read length not match\")\n\t\t\t\t}\n\t\t\t\tif string(a.p) != \"github.com\" {\n\t\t\t\t\treturn errors.New(\"read content not match\")\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"readAt len 12 offset 11\",\n\t\t\tb:    bs,\n\t\t\targs: args{\n\t\t\t\tp:   make([]byte, 12),\n\t\t\t\toff: 11,\n\t\t\t},\n\t\t\twant: func(a args, n int, err error) error {\n\t\t\t\tif n != len(a.p) {\n\t\t\t\t\treturn errors.New(\"read length not match\")\n\t\t\t\t}\n\t\t\t\tif string(a.p) != \"OpenListTeam\" {\n\t\t\t\t\treturn errors.New(\"read content not match\")\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"readAt len 50 offset 24\",\n\t\t\tb:    bs,\n\t\t\targs: args{\n\t\t\t\tp:   make([]byte, 50),\n\t\t\t\toff: 24,\n\t\t\t},\n\t\t\twant: func(a args, n int, err error) error {\n\t\t\t\tif n != int(bs.Size()-a.off) {\n\t\t\t\t\treturn errors.New(\"read length not match\")\n\t\t\t\t}\n\t\t\t\tif string(a.p[:n]) != \"OpenList\" {\n\t\t\t\t\treturn errors.New(\"read content not match\")\n\t\t\t\t}\n\t\t\t\tif err != io.EOF {\n\t\t\t\t\treturn errors.New(\"expect eof\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.b.ReadAt(tt.args.p, tt.args.off)\n\t\t\tif err := tt.want(tt.args, got, err); err != nil {\n\t\t\t\tt.Errorf(\"Bytes.ReadAt() error = %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/buffer/file.go",
    "content": "package buffer\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n)\n\ntype PeekFile struct {\n\tpeek   *Reader\n\tfile   *os.File\n\toffset int64\n\tsize   int64\n}\n\nfunc (p *PeekFile) Read(b []byte) (n int, err error) {\n\tn, err = p.ReadAt(b, p.offset)\n\tif n > 0 {\n\t\tp.offset += int64(n)\n\t}\n\treturn n, err\n}\n\nfunc (p *PeekFile) ReadAt(b []byte, off int64) (n int, err error) {\n\tif off < p.peek.Size() {\n\t\tn, err = p.peek.ReadAt(b, off)\n\t\tif err == nil || n == len(b) {\n\t\t\treturn n, nil\n\t\t}\n\t\t// EOF\n\t}\n\tvar nn int\n\tnn, err = p.file.ReadAt(b[n:], off+int64(n)-p.peek.Size())\n\treturn n + nn, err\n}\n\nfunc (p *PeekFile) Seek(offset int64, whence int) (int64, error) {\n\tswitch whence {\n\tcase io.SeekStart:\n\tcase io.SeekCurrent:\n\t\tif offset == 0 {\n\t\t\treturn p.offset, nil\n\t\t}\n\t\toffset = p.offset + offset\n\tcase io.SeekEnd:\n\t\toffset = p.size + offset\n\tdefault:\n\t\treturn 0, errors.New(\"Seek: invalid whence\")\n\t}\n\n\tif offset < 0 || offset > p.size {\n\t\treturn 0, errors.New(\"Seek: invalid offset\")\n\t}\n\tif offset <= p.peek.Size() {\n\t\t_, err := p.peek.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\t_, err = p.file.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t} else {\n\t\t_, err := p.peek.Seek(p.peek.Size(), io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\t_, err = p.file.Seek(offset-p.peek.Size(), io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\tp.offset = offset\n\treturn offset, nil\n}\n\nfunc (p *PeekFile) Size() int64 {\n\treturn p.size\n}\n\nfunc NewPeekFile(peek *Reader, file *os.File) (*PeekFile, error) {\n\tstat, err := file.Stat()\n\tif err == nil {\n\t\treturn &PeekFile{peek: peek, file: file, size: stat.Size() + peek.Size()}, nil\n\t}\n\treturn nil, err\n}\n"
  },
  {
    "path": "pkg/chanio/chanio.go",
    "content": "package chanio\n\nimport (\n\t\"io\"\n\t\"sync/atomic\"\n)\n\ntype ChanIO struct {\n\tcl  atomic.Bool\n\tc   chan []byte\n\tbuf []byte\n}\n\nfunc New() *ChanIO {\n\treturn &ChanIO{\n\t\tcl:  atomic.Bool{},\n\t\tc:   make(chan []byte),\n\t\tbuf: make([]byte, 0),\n\t}\n}\n\nfunc (c *ChanIO) Read(p []byte) (int, error) {\n\tif c.cl.Load() {\n\t\tif len(c.buf) == 0 {\n\t\t\treturn 0, io.EOF\n\t\t}\n\t\tn := copy(p, c.buf)\n\t\tif len(c.buf) > n {\n\t\t\tc.buf = c.buf[n:]\n\t\t} else {\n\t\t\tc.buf = make([]byte, 0)\n\t\t}\n\t\treturn n, nil\n\t}\n\tfor len(c.buf) < len(p) && !c.cl.Load() {\n\t\tc.buf = append(c.buf, <-c.c...)\n\t}\n\tn := copy(p, c.buf)\n\tif len(c.buf) > n {\n\t\tc.buf = c.buf[n:]\n\t} else {\n\t\tc.buf = make([]byte, 0)\n\t}\n\treturn n, nil\n}\n\nfunc (c *ChanIO) Write(p []byte) (int, error) {\n\tif c.cl.Load() {\n\t\treturn 0, io.ErrClosedPipe\n\t}\n\tc.c <- p\n\treturn len(p), nil\n}\n\nfunc (c *ChanIO) Close() error {\n\tif c.cl.Load() {\n\t\treturn io.ErrClosedPipe\n\t}\n\tc.cl.Store(true)\n\tclose(c.c)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cookie/cookie.go",
    "content": "package cookie\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc Parse(str string) []*http.Cookie {\n\theader := http.Header{}\n\theader.Add(\"Cookie\", str)\n\trequest := http.Request{Header: header}\n\treturn request.Cookies()\n}\n\nfunc ToString(cookies []*http.Cookie) string {\n\tif cookies == nil {\n\t\treturn \"\"\n\t}\n\tcookieStrings := make([]string, len(cookies))\n\tfor i, cookie := range cookies {\n\t\tcookieStrings[i] = cookie.String()\n\t}\n\treturn strings.Join(cookieStrings, \";\")\n}\n\nfunc SetCookie(cookies []*http.Cookie, name, value string) []*http.Cookie {\n\tfor i, cookie := range cookies {\n\t\tif cookie.Name == name {\n\t\t\tcookies[i].Value = value\n\t\t\treturn cookies\n\t\t}\n\t}\n\tcookies = append(cookies, &http.Cookie{Name: name, Value: value})\n\treturn cookies\n}\n\nfunc GetCookie(cookies []*http.Cookie, name string) *http.Cookie {\n\tfor _, cookie := range cookies {\n\t\tif cookie.Name == name {\n\t\t\treturn cookie\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc SetStr(cookiesStr, name, value string) string {\n\tcookies := Parse(cookiesStr)\n\tcookies = SetCookie(cookies, name, value)\n\treturn ToString(cookies)\n}\n\nfunc GetStr(cookiesStr, name string) string {\n\tcookies := Parse(cookiesStr)\n\tcookie := GetCookie(cookies, name)\n\tif cookie == nil {\n\t\treturn \"\"\n\t}\n\treturn cookie.Value\n}\n"
  },
  {
    "path": "pkg/cron/cron.go",
    "content": "package cron\n\nimport \"time\"\n\ntype Cron struct {\n\td  time.Duration\n\tch chan struct{}\n}\n\nfunc NewCron(d time.Duration) *Cron {\n\treturn &Cron{\n\t\td:  d,\n\t\tch: make(chan struct{}),\n\t}\n}\n\nfunc (c *Cron) Do(f func()) {\n\tgo func() {\n\t\tticker := time.NewTicker(c.d)\n\t\tdefer ticker.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tf()\n\t\t\tcase <-c.ch:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc (c *Cron) Stop() {\n\tselect {\n\tcase _, _ = <-c.ch:\n\tdefault:\n\t\tc.ch <- struct{}{}\n\t\tclose(c.ch)\n\t}\n}\n"
  },
  {
    "path": "pkg/cron/cron_test.go",
    "content": "package cron\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCron(t *testing.T) {\n\tc := NewCron(time.Second)\n\tc.Do(func() {\n\t\tt.Logf(\"cron log\")\n\t})\n\ttime.Sleep(time.Second * 3)\n\tc.Stop()\n\tc.Stop()\n}\n"
  },
  {
    "path": "pkg/errgroup/errgroup.go",
    "content": "package errgroup\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/avast/retry-go\"\n)\n\ntype token struct{}\ntype Group struct {\n\tcancel func(error)\n\tctx    context.Context\n\topts   []retry.Option\n\n\tsuccess uint64\n\n\twg  sync.WaitGroup\n\tsem chan token\n\n\tstartChan chan token\n}\n\nfunc NewGroupWithContext(ctx context.Context, limit int, retryOpts ...retry.Option) (*Group, context.Context) {\n\tctx, cancel := context.WithCancelCause(ctx)\n\treturn (&Group{cancel: cancel, ctx: ctx, opts: append(retryOpts, retry.Context(ctx))}).SetLimit(limit), ctx\n}\n\n// OrderedGroup\n// 使得Lifecycle.Before是有序且线程安全\nfunc NewOrderedGroupWithContext(ctx context.Context, limit int, retryOpts ...retry.Option) (*Group, context.Context) {\n\tgroup, ctx := NewGroupWithContext(ctx, limit, retryOpts...)\n\tgroup.startChan = make(chan token, 1)\n\treturn group, ctx\n}\n\nfunc (g *Group) done() {\n\tif g.sem != nil {\n\t\t<-g.sem\n\t}\n\tg.wg.Done()\n\tatomic.AddUint64(&g.success, 1)\n}\n\nfunc (g *Group) Wait() error {\n\tg.wg.Wait()\n\treturn context.Cause(g.ctx)\n}\n\nfunc (g *Group) Go(do func(ctx context.Context) error) {\n\tg.GoWithLifecycle(Lifecycle{Do: do})\n}\n\ntype Lifecycle struct {\n\t// Before在OrderedGroup是有序且线程安全的\n\t// 只会被调用一次\n\tBefore func(ctx context.Context) (err error)\n\t// 如果Before返回err就不调用Do\n\tDo func(ctx context.Context) (err error)\n\t// 最后调用一次After\n\tAfter func(err error)\n}\n\nfunc (g *Group) GoWithLifecycle(lifecycle Lifecycle) {\n\tif g.startChan != nil {\n\t\tselect {\n\t\tcase <-g.ctx.Done():\n\t\t\treturn\n\t\tcase g.startChan <- token{}:\n\t\t}\n\t}\n\n\tif g.sem != nil {\n\t\tselect {\n\t\tcase <-g.ctx.Done():\n\t\t\treturn\n\t\tcase g.sem <- token{}:\n\t\t}\n\t}\n\n\tg.wg.Add(1)\n\tgo func() {\n\t\tdefer g.done()\n\t\tvar err error\n\t\tif lifecycle.Before != nil {\n\t\t\terr = lifecycle.Before(g.ctx)\n\t\t}\n\t\tif err == nil {\n\t\t\tif g.startChan != nil {\n\t\t\t\t<-g.startChan\n\t\t\t}\n\t\t\terr = retry.Do(func() error { return lifecycle.Do(g.ctx) }, g.opts...)\n\t\t}\n\t\tif lifecycle.After != nil {\n\t\t\tlifecycle.After(err)\n\t\t}\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-g.ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tg.cancel(err)\n\t\t\t}\n\t\t}\n\t}()\n\n}\n\nfunc (g *Group) TryGo(f func(ctx context.Context) error) bool {\n\tif g.sem != nil {\n\t\tselect {\n\t\tcase g.sem <- token{}:\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\tg.wg.Add(1)\n\tgo func() {\n\t\tdefer g.done()\n\t\tif err := retry.Do(func() error { return f(g.ctx) }, g.opts...); err != nil {\n\t\t\tg.cancel(err)\n\t\t}\n\t}()\n\treturn true\n}\n\nfunc (g *Group) SetLimit(n int) *Group {\n\tif len(g.sem) != 0 {\n\t\tpanic(fmt.Errorf(\"errgroup: modify limit while %v goroutines in the group are still active\", len(g.sem)))\n\t}\n\tif n > 0 {\n\t\tg.sem = make(chan token, n)\n\t} else {\n\t\tg.sem = nil\n\t}\n\treturn g\n}\n\nfunc (g *Group) Success() uint64 {\n\treturn atomic.LoadUint64(&g.success)\n}\n\nfunc (g *Group) Err() error {\n\treturn context.Cause(g.ctx)\n}\n"
  },
  {
    "path": "pkg/generic/queue.go",
    "content": "package generic\n\ntype Queue[T any] struct {\n\tqueue []T\n}\n\nfunc NewQueue[T any]() *Queue[T] {\n\treturn &Queue[T]{queue: make([]T, 0)}\n}\n\nfunc (q *Queue[T]) Push(v T) {\n\tq.queue = append(q.queue, v)\n}\n\nfunc (q *Queue[T]) Pop() T {\n\tv := q.queue[0]\n\tq.queue = q.queue[1:]\n\treturn v\n}\n\nfunc (q *Queue[T]) Len() int {\n\treturn len(q.queue)\n}\n\nfunc (q *Queue[T]) IsEmpty() bool {\n\treturn len(q.queue) == 0\n}\n\nfunc (q *Queue[T]) Clear() {\n\tq.queue = nil\n}\n\nfunc (q *Queue[T]) Peek() T {\n\treturn q.queue[0]\n}\n\nfunc (q *Queue[T]) PeekN(n int) []T {\n\treturn q.queue[:n]\n}\n\nfunc (q *Queue[T]) PopN(n int) []T {\n\tv := q.queue[:n]\n\tq.queue = q.queue[n:]\n\treturn v\n}\n\nfunc (q *Queue[T]) PopAll() []T {\n\tv := q.queue\n\tq.queue = nil\n\treturn v\n}\n\nfunc (q *Queue[T]) PopWhile(f func(T) bool) []T {\n\tvar i int\n\tfor i = 0; i < len(q.queue); i++ {\n\t\tif !f(q.queue[i]) {\n\t\t\tbreak\n\t\t}\n\t}\n\tv := q.queue[:i]\n\tq.queue = q.queue[i:]\n\treturn v\n}\n\nfunc (q *Queue[T]) PopUntil(f func(T) bool) []T {\n\tvar i int\n\tfor i = 0; i < len(q.queue); i++ {\n\t\tif f(q.queue[i]) {\n\t\t\tbreak\n\t\t}\n\t}\n\tv := q.queue[:i]\n\tq.queue = q.queue[i:]\n\treturn v\n}\n"
  },
  {
    "path": "pkg/generic_sync/map.go",
    "content": "// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage generic_sync\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"unsafe\"\n)\n\n// MapOf is like a Go map[interface{}]interface{} but is safe for concurrent use\n// by multiple goroutines without additional locking or coordination.\n// Loads, stores, and deletes run in amortized constant time.\n//\n// The MapOf type is specialized. Most code should use a plain Go map instead,\n// with separate locking or coordination, for better type safety and to make it\n// easier to maintain other invariants along with the map content.\n//\n// The MapOf type is optimized for two common use cases: (1) when the entry for a given\n// key is only ever written once but read many times, as in caches that only grow,\n// or (2) when multiple goroutines read, write, and overwrite entries for disjoint\n// sets of keys. In these two cases, use of a MapOf may significantly reduce lock\n// contention compared to a Go map paired with a separate Mutex or RWMutex.\n//\n// The zero MapOf is empty and ready for use. A MapOf must not be copied after first use.\ntype MapOf[K comparable, V any] struct {\n\tmu sync.Mutex\n\n\t// read contains the portion of the map's contents that are safe for\n\t// concurrent access (with or without mu held).\n\t//\n\t// The read field itself is always safe to load, but must only be stored with\n\t// mu held.\n\t//\n\t// Entries stored in read may be updated concurrently without mu, but updating\n\t// a previously-expunged entry requires that the entry be copied to the dirty\n\t// map and unexpunged with mu held.\n\tread atomic.Value // readOnly\n\n\t// dirty contains the portion of the map's contents that require mu to be\n\t// held. To ensure that the dirty map can be promoted to the read map quickly,\n\t// it also includes all of the non-expunged entries in the read map.\n\t//\n\t// Expunged entries are not stored in the dirty map. An expunged entry in the\n\t// clean map must be unexpunged and added to the dirty map before a new value\n\t// can be stored to it.\n\t//\n\t// If the dirty map is nil, the next write to the map will initialize it by\n\t// making a shallow copy of the clean map, omitting stale entries.\n\tdirty map[K]*entry[V]\n\n\t// misses counts the number of loads since the read map was last updated that\n\t// needed to lock mu to determine whether the key was present.\n\t//\n\t// Once enough misses have occurred to cover the cost of copying the dirty\n\t// map, the dirty map will be promoted to the read map (in the unamended\n\t// state) and the next store to the map will make a new dirty copy.\n\tmisses int\n}\n\n// readOnly is an immutable struct stored atomically in the MapOf.read field.\ntype readOnly[K comparable, V any] struct {\n\tm       map[K]*entry[V]\n\tamended bool // true if the dirty map contains some key not in m.\n}\n\n// expunged is an arbitrary pointer that marks entries which have been deleted\n// from the dirty map.\nvar expunged = unsafe.Pointer(new(interface{}))\n\n// An entry is a slot in the map corresponding to a particular key.\ntype entry[V any] struct {\n\t// p points to the interface{} value stored for the entry.\n\t//\n\t// If p == nil, the entry has been deleted and m.dirty == nil.\n\t//\n\t// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry\n\t// is missing from m.dirty.\n\t//\n\t// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty\n\t// != nil, in m.dirty[key].\n\t//\n\t// An entry can be deleted by atomic replacement with nil: when m.dirty is\n\t// next created, it will atomically replace nil with expunged and leave\n\t// m.dirty[key] unset.\n\t//\n\t// An entry's associated value can be updated by atomic replacement, provided\n\t// p != expunged. If p == expunged, an entry's associated value can be updated\n\t// only after first setting m.dirty[key] = e so that lookups using the dirty\n\t// map find the entry.\n\tp unsafe.Pointer // *interface{}\n}\n\nfunc newEntry[V any](i V) *entry[V] {\n\treturn &entry[V]{p: unsafe.Pointer(&i)}\n}\n\n// Load returns the value stored in the map for a key, or nil if no\n// value is present.\n// The ok result indicates whether value was found in the map.\nfunc (m *MapOf[K, V]) Load(key K) (value V, ok bool) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\te, ok := read.m[key]\n\tif !ok && read.amended {\n\t\tm.mu.Lock()\n\t\t// Avoid reporting a spurious miss if m.dirty got promoted while we were\n\t\t// blocked on m.mu. (If further loads of the same key will not miss, it's\n\t\t// not worth copying the dirty map for this key.)\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\te, ok = read.m[key]\n\t\tif !ok && read.amended {\n\t\t\te, ok = m.dirty[key]\n\t\t\t// Regardless of whether the entry was present, record a miss: this key\n\t\t\t// will take the slow path until the dirty map is promoted to the read\n\t\t\t// map.\n\t\t\tm.missLocked()\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\tif !ok {\n\t\treturn value, false\n\t}\n\treturn e.load()\n}\n\nfunc (m *MapOf[K, V]) Has(key K) bool {\n\t_, ok := m.Load(key)\n\treturn ok\n}\n\nfunc (e *entry[V]) load() (value V, ok bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tif p == nil || p == expunged {\n\t\treturn value, false\n\t}\n\treturn *(*V)(p), true\n}\n\n// Store sets the value for a key.\nfunc (m *MapOf[K, V]) Store(key K, value V) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok && e.tryStore(&value) {\n\t\treturn\n\t}\n\n\tm.mu.Lock()\n\tread, _ = m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tif e.unexpungeLocked() {\n\t\t\t// The entry was previously expunged, which implies that there is a\n\t\t\t// non-nil dirty map and this entry is not in it.\n\t\t\tm.dirty[key] = e\n\t\t}\n\t\te.storeLocked(&value)\n\t} else if e, ok := m.dirty[key]; ok {\n\t\te.storeLocked(&value)\n\t} else {\n\t\tif !read.amended {\n\t\t\t// We're adding the first new key to the dirty map.\n\t\t\t// Make sure it is allocated and mark the read-only map as incomplete.\n\t\t\tm.dirtyLocked()\n\t\t\tm.read.Store(readOnly[K, V]{m: read.m, amended: true})\n\t\t}\n\t\tm.dirty[key] = newEntry(value)\n\t}\n\tm.mu.Unlock()\n}\n\n// tryStore stores a value if the entry has not been expunged.\n//\n// If the entry is expunged, tryStore returns false and leaves the entry\n// unchanged.\nfunc (e *entry[V]) tryStore(i *V) bool {\n\tfor {\n\t\tp := atomic.LoadPointer(&e.p)\n\t\tif p == expunged {\n\t\t\treturn false\n\t\t}\n\t\tif atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\n// unexpungeLocked ensures that the entry is not marked as expunged.\n//\n// If the entry was previously expunged, it must be added to the dirty map\n// before m.mu is unlocked.\nfunc (e *entry[V]) unexpungeLocked() (wasExpunged bool) {\n\treturn atomic.CompareAndSwapPointer(&e.p, expunged, nil)\n}\n\n// storeLocked unconditionally stores a value to the entry.\n//\n// The entry must be known not to be expunged.\nfunc (e *entry[V]) storeLocked(i *V) {\n\tatomic.StorePointer(&e.p, unsafe.Pointer(i))\n}\n\n// LoadOrStore returns the existing value for the key if present.\n// Otherwise, it stores and returns the given value.\n// The loaded result is true if the value was loaded, false if stored.\nfunc (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {\n\t// Avoid locking if it's a clean hit.\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tactual, loaded, ok := e.tryLoadOrStore(value)\n\t\tif ok {\n\t\t\treturn actual, loaded\n\t\t}\n\t}\n\n\tm.mu.Lock()\n\tread, _ = m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tif e.unexpungeLocked() {\n\t\t\tm.dirty[key] = e\n\t\t}\n\t\tactual, loaded, _ = e.tryLoadOrStore(value)\n\t} else if e, ok := m.dirty[key]; ok {\n\t\tactual, loaded, _ = e.tryLoadOrStore(value)\n\t\tm.missLocked()\n\t} else {\n\t\tif !read.amended {\n\t\t\t// We're adding the first new key to the dirty map.\n\t\t\t// Make sure it is allocated and mark the read-only map as incomplete.\n\t\t\tm.dirtyLocked()\n\t\t\tm.read.Store(readOnly[K, V]{m: read.m, amended: true})\n\t\t}\n\t\tm.dirty[key] = newEntry(value)\n\t\tactual, loaded = value, false\n\t}\n\tm.mu.Unlock()\n\n\treturn actual, loaded\n}\n\n// tryLoadOrStore atomically loads or stores a value if the entry is not\n// expunged.\n//\n// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and\n// returns with ok==false.\nfunc (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tif p == expunged {\n\t\treturn actual, false, false\n\t}\n\tif p != nil {\n\t\treturn *(*V)(p), true, true\n\t}\n\n\t// Copy the interface after the first load to make this method more amenable\n\t// to escape analysis: if we hit the \"load\" path or the entry is expunged, we\n\t// shouldn'V bother heap-allocating.\n\tic := i\n\tfor {\n\t\tif atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {\n\t\t\treturn i, false, true\n\t\t}\n\t\tp = atomic.LoadPointer(&e.p)\n\t\tif p == expunged {\n\t\t\treturn actual, false, false\n\t\t}\n\t\tif p != nil {\n\t\t\treturn *(*V)(p), true, true\n\t\t}\n\t}\n}\n\n// Delete deletes the value for a key.\nfunc (m *MapOf[K, V]) Delete(key K) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\te, ok := read.m[key]\n\tif !ok && read.amended {\n\t\tm.mu.Lock()\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\te, ok = read.m[key]\n\t\tif !ok && read.amended {\n\t\t\tdelete(m.dirty, key)\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\tif ok {\n\t\te.delete()\n\t}\n}\n\nfunc (e *entry[V]) delete() (hadValue bool) {\n\tfor {\n\t\tp := atomic.LoadPointer(&e.p)\n\t\tif p == nil || p == expunged {\n\t\t\treturn false\n\t\t}\n\t\tif atomic.CompareAndSwapPointer(&e.p, p, nil) {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\n// Range calls f sequentially for each key and value present in the map.\n// If f returns false, range stops the iteration.\n//\n// Range does not necessarily correspond to any consistent snapshot of the MapOf's\n// contents: no key will be visited more than once, but if the value for any key\n// is stored or deleted concurrently, Range may reflect any mapping for that key\n// from any point during the Range call.\n//\n// Range may be O(N) with the number of elements in the map even if f returns\n// false after a constant number of calls.\nfunc (m *MapOf[K, V]) Range(f func(key K, value V) bool) {\n\t// We need to be able to iterate over all of the keys that were already\n\t// present at the start of the call to Range.\n\t// If read.amended is false, then read.m satisfies that property without\n\t// requiring us to hold m.mu for a long time.\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif read.amended {\n\t\t// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)\n\t\t// (assuming the caller does not break out early), so a call to Range\n\t\t// amortizes an entire copy of the map: we can promote the dirty copy\n\t\t// immediately!\n\t\tm.mu.Lock()\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\tif read.amended {\n\t\t\tread = readOnly[K, V]{m: m.dirty}\n\t\t\tm.read.Store(read)\n\t\t\tm.dirty = nil\n\t\t\tm.misses = 0\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\n\tfor k, e := range read.m {\n\t\tv, ok := e.load()\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif !f(k, v) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// Values returns a slice of the values in the map.\nfunc (m *MapOf[K, V]) Values() []V {\n\tvar values []V\n\tm.Range(func(key K, value V) bool {\n\t\tvalues = append(values, value)\n\t\treturn true\n\t})\n\treturn values\n}\n\nfunc (m *MapOf[K, V]) Count() int {\n\treturn len(m.dirty)\n}\n\nfunc (m *MapOf[K, V]) Empty() bool {\n\treturn m.Count() == 0\n}\n\nfunc (m *MapOf[K, V]) ToMap() map[K]V {\n\tans := make(map[K]V)\n\tm.Range(func(key K, value V) bool {\n\t\tans[key] = value\n\t\treturn true\n\t})\n\treturn ans\n}\n\nfunc (m *MapOf[K, V]) Clear() {\n\tm.Range(func(key K, value V) bool {\n\t\tm.Delete(key)\n\t\treturn true\n\t})\n}\n\nfunc (m *MapOf[K, V]) missLocked() {\n\tm.misses++\n\tif m.misses < len(m.dirty) {\n\t\treturn\n\t}\n\tm.read.Store(readOnly[K, V]{m: m.dirty})\n\tm.dirty = nil\n\tm.misses = 0\n}\n\nfunc (m *MapOf[K, V]) dirtyLocked() {\n\tif m.dirty != nil {\n\t\treturn\n\t}\n\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tm.dirty = make(map[K]*entry[V], len(read.m))\n\tfor k, e := range read.m {\n\t\tif !e.tryExpungeLocked() {\n\t\t\tm.dirty[k] = e\n\t\t}\n\t}\n}\n\nfunc (e *entry[V]) tryExpungeLocked() (isExpunged bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tfor p == nil {\n\t\tif atomic.CompareAndSwapPointer(&e.p, nil, expunged) {\n\t\t\treturn true\n\t\t}\n\t\tp = atomic.LoadPointer(&e.p)\n\t}\n\treturn p == expunged\n}\n"
  },
  {
    "path": "pkg/generic_sync/map_test.go",
    "content": "// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage generic_sync_test\n\nimport (\n\t\"math/rand\"\n\t\"runtime\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync\"\n)\n\nfunc TestConcurrentRange(t *testing.T) {\n\tconst mapSize = 1 << 10\n\n\tm := new(generic_sync.MapOf[int64, int64])\n\tfor n := int64(1); n <= mapSize; n++ {\n\t\tm.Store(n, int64(n))\n\t}\n\n\tdone := make(chan struct{})\n\tvar wg sync.WaitGroup\n\tdefer func() {\n\t\tclose(done)\n\t\twg.Wait()\n\t}()\n\tfor g := int64(runtime.GOMAXPROCS(0)); g > 0; g-- {\n\t\tr := rand.New(rand.NewSource(g))\n\t\twg.Add(1)\n\t\tgo func(g int64) {\n\t\t\tdefer wg.Done()\n\t\t\tfor i := int64(0); ; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tfor n := int64(1); n < mapSize; n++ {\n\t\t\t\t\tif r.Int63n(mapSize) == 0 {\n\t\t\t\t\t\tm.Store(n, n*i*g)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tm.Load(n)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}(g)\n\t}\n\n\titers := 1 << 10\n\tif testing.Short() {\n\t\titers = 16\n\t}\n\tfor n := iters; n > 0; n-- {\n\t\tseen := make(map[int64]bool, mapSize)\n\n\t\tm.Range(func(k, v int64) bool {\n\t\t\tif v%k != 0 {\n\t\t\t\tt.Fatalf(\"while Storing multiples of %v, Range saw value %v\", k, v)\n\t\t\t}\n\t\t\tif seen[k] {\n\t\t\t\tt.Fatalf(\"Range visited key %v twice\", k)\n\t\t\t}\n\t\t\tseen[k] = true\n\t\t\treturn true\n\t\t})\n\n\t\tif len(seen) != mapSize {\n\t\t\tt.Fatalf(\"Range visited %v elements of %v-element MapOf\", len(seen), mapSize)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/http_range/range.go",
    "content": "// Package http_range implements http range parsing.\npackage http_range\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Range specifies the byte range to be sent to the client.\ntype Range struct {\n\tStart  int64\n\tLength int64 // limit of bytes to read, -1 for unlimited\n}\n\n// ContentRange returns Content-Range header value.\nfunc (r Range) ContentRange(size int64) string {\n\treturn fmt.Sprintf(\"bytes %d-%d/%d\", r.Start, r.Start+r.Length-1, size)\n}\n\nvar (\n\t// ErrNoOverlap is returned by ParseRange if first-byte-pos of\n\t// all the byte-range-spec values is greater than the content size.\n\tErrNoOverlap = errors.New(\"invalid range: failed to overlap\")\n\n\t// ErrInvalid is returned by ParseRange on invalid input.\n\tErrInvalid = errors.New(\"invalid range\")\n)\n\n// ParseRange parses a Range header string as per RFC 7233.\n// ErrNoOverlap is returned if none of the ranges overlap.\n// ErrInvalid is returned if s is invalid range.\nfunc ParseRange(s string, size int64) ([]Range, error) { // nolint:gocognit\n\tif s == \"\" {\n\t\treturn nil, nil // header not present\n\t}\n\tconst b = \"bytes=\"\n\tif !strings.HasPrefix(s, b) {\n\t\treturn nil, ErrInvalid\n\t}\n\tvar ranges []Range\n\tnoOverlap := false\n\tfor _, ra := range strings.Split(s[len(b):], \",\") {\n\t\tra = textproto.TrimString(ra)\n\t\tif ra == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\ti := strings.Index(ra, \"-\")\n\t\tif i < 0 {\n\t\t\treturn nil, ErrInvalid\n\t\t}\n\t\tstart, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])\n\t\tvar r Range\n\t\tif start == \"\" {\n\t\t\t// If no start is specified, end specifies the\n\t\t\t// range start relative to the end of the file,\n\t\t\t// and we are dealing with <suffix-length>\n\t\t\t// which has to be a non-negative integer as per\n\t\t\t// RFC 7233 Section 2.1 \"Byte-Ranges\".\n\t\t\tif end == \"\" || end[0] == '-' {\n\t\t\t\treturn nil, ErrInvalid\n\t\t\t}\n\t\t\ti, err := strconv.ParseInt(end, 10, 64)\n\t\t\tif i < 0 || err != nil {\n\t\t\t\treturn nil, ErrInvalid\n\t\t\t}\n\t\t\tif i > size {\n\t\t\t\ti = size\n\t\t\t}\n\t\t\tr.Start = size - i\n\t\t\tr.Length = size - r.Start\n\t\t} else {\n\t\t\ti, err := strconv.ParseInt(start, 10, 64)\n\t\t\tif err != nil || i < 0 {\n\t\t\t\treturn nil, ErrInvalid\n\t\t\t}\n\t\t\tif i >= size {\n\t\t\t\t// If the range begins after the size of the content,\n\t\t\t\t// then it does not overlap.\n\t\t\t\tnoOverlap = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.Start = i\n\t\t\tif end == \"\" {\n\t\t\t\t// If no end is specified, range extends to end of the file.\n\t\t\t\tr.Length = size - r.Start\n\t\t\t} else {\n\t\t\t\ti, err := strconv.ParseInt(end, 10, 64)\n\t\t\t\tif err != nil || r.Start > i {\n\t\t\t\t\treturn nil, ErrInvalid\n\t\t\t\t}\n\t\t\t\tif i >= size {\n\t\t\t\t\ti = size - 1\n\t\t\t\t}\n\t\t\t\tr.Length = i - r.Start + 1\n\t\t\t}\n\t\t}\n\t\tranges = append(ranges, r)\n\t}\n\tif noOverlap && len(ranges) == 0 {\n\t\t// The specified ranges did not overlap with the content.\n\t\treturn nil, ErrNoOverlap\n\t}\n\treturn ranges, nil\n}\n\n// ParseContentRange this function parse content-range in http response\nfunc ParseContentRange(s string) (start, end int64, err error) {\n\tif s == \"\" {\n\t\treturn 0, 0, ErrInvalid\n\t}\n\tconst b = \"bytes \"\n\tif !strings.HasPrefix(s, b) {\n\t\treturn 0, 0, ErrInvalid\n\t}\n\tp1 := strings.Index(s, \"-\")\n\tp2 := strings.Index(s, \"/\")\n\tif p1 < 0 || p2 < 0 {\n\t\treturn 0, 0, ErrInvalid\n\t}\n\tstartStr, endStr := textproto.TrimString(s[len(b):p1]), textproto.TrimString(s[p1+1:p2])\n\tstart, startErr := strconv.ParseInt(startStr, 10, 64)\n\tend, endErr := strconv.ParseInt(endStr, 10, 64)\n\n\treturn start, end, errors.Join(startErr, endErr)\n}\n\nfunc (r Range) MimeHeader(contentType string, size int64) textproto.MIMEHeader {\n\treturn textproto.MIMEHeader{\n\t\t\"Content-Range\": {r.ContentRange(size)},\n\t\t\"Content-Type\":  {contentType},\n\t}\n}\n\n// ApplyRangeToHttpHeader for http request header\nfunc ApplyRangeToHttpHeader(p Range, headerRef http.Header) http.Header {\n\theader := headerRef\n\tif header == nil {\n\t\theader = http.Header{}\n\t}\n\tif p.Start == 0 && p.Length < 0 {\n\t\theader.Del(\"Range\")\n\t} else {\n\t\tend := \"\"\n\t\tif p.Length >= 0 {\n\t\t\tend = strconv.FormatInt(p.Start+p.Length-1, 10)\n\t\t}\n\t\theader.Set(\"Range\", fmt.Sprintf(\"bytes=%v-%v\", p.Start, end))\n\t}\n\treturn header\n}\n"
  },
  {
    "path": "pkg/mq/mq.go",
    "content": "package mq\n\nimport (\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic\"\n)\n\ntype Message[T any] struct {\n\tContent T\n}\n\ntype BasicConsumer[T any] func(Message[T])\ntype AllConsumer[T any] func([]Message[T])\n\ntype MQ[T any] interface {\n\tPublish(Message[T])\n\tConsume(BasicConsumer[T])\n\tConsumeAll(AllConsumer[T])\n\tClear()\n\tLen() int\n}\n\ntype inMemoryMQ[T any] struct {\n\tqueue generic.Queue[Message[T]]\n\tsync.Mutex\n}\n\nfunc NewInMemoryMQ[T any]() MQ[T] {\n\treturn &inMemoryMQ[T]{queue: *generic.NewQueue[Message[T]]()}\n}\n\nfunc (mq *inMemoryMQ[T]) Publish(msg Message[T]) {\n\tmq.Lock()\n\tdefer mq.Unlock()\n\tmq.queue.Push(msg)\n}\n\nfunc (mq *inMemoryMQ[T]) Consume(consumer BasicConsumer[T]) {\n\tmq.Lock()\n\tdefer mq.Unlock()\n\tfor !mq.queue.IsEmpty() {\n\t\tconsumer(mq.queue.Pop())\n\t}\n}\n\nfunc (mq *inMemoryMQ[T]) ConsumeAll(consumer AllConsumer[T]) {\n\tmq.Lock()\n\tdefer mq.Unlock()\n\tconsumer(mq.queue.PopAll())\n}\n\nfunc (mq *inMemoryMQ[T]) Clear() {\n\tmq.Lock()\n\tdefer mq.Unlock()\n\tmq.queue.Clear()\n}\n\nfunc (mq *inMemoryMQ[T]) Len() int {\n\tmq.Lock()\n\tdefer mq.Unlock()\n\treturn mq.queue.Len()\n}\n"
  },
  {
    "path": "pkg/pool/pool.go",
    "content": "package pool\n\nimport \"sync\"\n\ntype Pool[T any] struct {\n\tNew    func() T\n\tMaxCap int\n\n\tcache []T\n\tmu    sync.Mutex\n}\n\nfunc (p *Pool[T]) Get() T {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tif len(p.cache) == 0 {\n\t\treturn p.New()\n\t}\n\titem := p.cache[len(p.cache)-1]\n\tp.cache = p.cache[:len(p.cache)-1]\n\treturn item\n}\n\nfunc (p *Pool[T]) Put(item T) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tif p.MaxCap == 0 || len(p.cache) < int(p.MaxCap) {\n\t\tp.cache = append(p.cache, item)\n\t}\n}\n\nfunc (p *Pool[T]) Reset() {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tclear(p.cache)\n\tp.cache = nil\n}\n"
  },
  {
    "path": "pkg/qbittorrent/client.go",
    "content": "package qbittorrent\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype Client interface {\n\tAddFromLink(link string, savePath string, id string) error\n\tGetInfo(id string) (TorrentInfo, error)\n\tGetFiles(id string) ([]FileInfo, error)\n\tDelete(id string, deleteFiles bool) error\n}\n\ntype client struct {\n\turl    *url.URL\n\tclient http.Client\n\tClient\n}\n\nfunc New(webuiUrl string) (Client, error) {\n\tu, err := url.Parse(webuiUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjar, err := cookiejar.New(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttransport := &http.Transport{\n\t\tMaxIdleConns:        10,\n\t\tMaxIdleConnsPerHost: 2,\n\t\tIdleConnTimeout:     30 * time.Second,\n\t\tDisableKeepAlives:   false, // Enable connection reuse\n\t}\n\t\n\tvar c = &client{\n\t\turl: u,\n\t\tclient: http.Client{\n\t\t\tJar:       jar,\n\t\t\tTransport: transport,\n\t\t\tTimeout:   30 * time.Second, // Set overall timeout\n\t\t},\n\t}\n\n\terr = c.checkAuthorization()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c, nil\n}\n\nfunc (c *client) checkAuthorization() error {\n\t// check authorization\n\tif c.authorized() {\n\t\treturn nil\n\t}\n\n\t// check authorization after logging in\n\terr := c.login()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif c.authorized() {\n\t\treturn nil\n\t}\n\treturn errors.New(\"unauthorized qbittorrent url\")\n}\n\nfunc (c *client) authorized() bool {\n\tresp, err := c.post(\"/api/v2/app/version\", nil)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer resp.Body.Close()\n\treturn resp.StatusCode == 200 // the status code will be 403 if not authorized\n}\n\nfunc (c *client) login() error {\n\t// prepare HTTP request\n\tv := url.Values{}\n\tv.Set(\"username\", c.url.User.Username())\n\tpasswd, _ := c.url.User.Password()\n\tv.Set(\"password\", passwd)\n\tresp, err := c.post(\"/api/v2/auth/login\", v)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t// check result\n\tbody := make([]byte, 2)\n\t_, err = resp.Body.Read(body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif string(body) != \"Ok\" {\n\t\treturn errors.New(\"failed to login into qBittorrent webui with url: \" + c.url.String())\n\t}\n\treturn nil\n}\n\nfunc (c *client) post(path string, data url.Values) (*http.Response, error) {\n\tu := c.url.JoinPath(path)\n\tu.User = nil // remove userinfo for requests\n\n\treq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader([]byte(data.Encode())))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif data != nil {\n\t\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Cookies() != nil {\n\t\tc.client.Jar.SetCookies(u, resp.Cookies())\n\t}\n\treturn resp, nil\n}\n\nfunc (c *client) AddFromLink(link string, savePath string, id string) error {\n\terr := c.checkAuthorization()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbuf := new(bytes.Buffer)\n\twriter := multipart.NewWriter(buf)\n\n\taddField := func(name string, value string) {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = writer.WriteField(name, value)\n\t}\n\taddField(\"urls\", link)\n\taddField(\"savepath\", savePath)\n\taddField(\"tags\", \"openlist-\"+id)\n\taddField(\"autoTMM\", \"false\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = writer.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu := c.url.JoinPath(\"/api/v2/torrents/add\")\n\tu.User = nil // remove userinfo for requests\n\treq, err := http.NewRequest(http.MethodPost, u.String(), buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t// check result\n\tbody := make([]byte, 2)\n\t_, err = resp.Body.Read(body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != 200 || string(body) != \"Ok\" {\n\t\treturn errors.New(\"failed to add qBittorrent task: \" + link)\n\t}\n\treturn nil\n}\n\ntype TorrentStatus string\n\nconst (\n\tERROR              TorrentStatus = \"error\"\n\tMISSINGFILES       TorrentStatus = \"missingFiles\"\n\tUPLOADING          TorrentStatus = \"uploading\"\n\tPAUSEDUP           TorrentStatus = \"pausedUP\"\n\tQUEUEDUP           TorrentStatus = \"queuedUP\"\n\tSTALLEDUP          TorrentStatus = \"stalledUP\"\n\tCHECKINGUP         TorrentStatus = \"checkingUP\"\n\tFORCEDUP           TorrentStatus = \"forcedUP\"\n\tALLOCATING         TorrentStatus = \"allocating\"\n\tDOWNLOADING        TorrentStatus = \"downloading\"\n\tMETADL             TorrentStatus = \"metaDL\"\n\tPAUSEDDL           TorrentStatus = \"pausedDL\"\n\tQUEUEDDL           TorrentStatus = \"queuedDL\"\n\tSTALLEDDL          TorrentStatus = \"stalledDL\"\n\tCHECKINGDL         TorrentStatus = \"checkingDL\"\n\tFORCEDDL           TorrentStatus = \"forcedDL\"\n\tCHECKINGRESUMEDATA TorrentStatus = \"checkingResumeData\"\n\tMOVING             TorrentStatus = \"moving\"\n\tUNKNOWN            TorrentStatus = \"unknown\"\n)\n\n// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go\ntype TorrentInfo struct {\n\tAddedOn           int           `json:\"added_on\"`           // 将 torrent 添加到客户端的时间（Unix Epoch）\n\tAmountLeft        int64         `json:\"amount_left\"`        // 剩余大小（字节）\n\tAutoTmm           bool          `json:\"auto_tmm\"`           // 此 torrent 是否由 Automatic Torrent Management 管理\n\tAvailability      float64       `json:\"availability\"`       // 当前百分比\n\tCategory          string        `json:\"category\"`           //\n\tCompleted         int64         `json:\"completed\"`          // 完成的传输数据量（字节）\n\tCompletionOn      int           `json:\"completion_on\"`      // Torrent 完成的时间（Unix Epoch）\n\tContentPath       string        `json:\"content_path\"`       // torrent 内容的绝对路径（多文件 torrent 的根路径，单文件 torrent 的绝对文件路径）\n\tDlLimit           int           `json:\"dl_limit\"`           // Torrent 下载速度限制（字节/秒）\n\tDlspeed           int           `json:\"dlspeed\"`            // Torrent 下载速度（字节/秒）\n\tDownloaded        int64         `json:\"downloaded\"`         // 已经下载大小\n\tDownloadedSession int64         `json:\"downloaded_session\"` // 此会话下载的数据量\n\tEta               int           `json:\"eta\"`                //\n\tFLPiecePrio       bool          `json:\"f_l_piece_prio\"`     // 如果第一个最后一块被优先考虑，则为true\n\tForceStart        bool          `json:\"force_start\"`        // 如果为此 torrent 启用了强制启动，则为true\n\tHash              string        `json:\"hash\"`               //\n\tLastActivity      int           `json:\"last_activity\"`      // 上次活跃的时间（Unix Epoch）\n\tMagnetURI         string        `json:\"magnet_uri\"`         // 与此 torrent 对应的 Magnet URI\n\tMaxRatio          float64       `json:\"max_ratio\"`          // 种子/上传停止种子前的最大共享比率\n\tMaxSeedingTime    int           `json:\"max_seeding_time\"`   // 停止种子种子前的最长种子时间（秒）\n\tName              string        `json:\"name\"`               //\n\tNumComplete       int           `json:\"num_complete\"`       //\n\tNumIncomplete     int           `json:\"num_incomplete\"`     //\n\tNumLeechs         int           `json:\"num_leechs\"`         // 连接到的 leechers 的数量\n\tNumSeeds          int           `json:\"num_seeds\"`          // 连接到的种子数\n\tPriority          int           `json:\"priority\"`           // 速度优先。如果队列被禁用或 torrent 处于种子模式，则返回 -1\n\tProgress          float64       `json:\"progress\"`           // 进度\n\tRatio             float64       `json:\"ratio\"`              // Torrent 共享比率\n\tRatioLimit        int           `json:\"ratio_limit\"`        //\n\tSavePath          string        `json:\"save_path\"`\n\tSeedingTime       int           `json:\"seeding_time\"`       // Torrent 完成用时（秒）\n\tSeedingTimeLimit  int           `json:\"seeding_time_limit\"` // max_seeding_time\n\tSeenComplete      int           `json:\"seen_complete\"`      // 上次 torrent 完成的时间\n\tSeqDl             bool          `json:\"seq_dl\"`             // 如果启用顺序下载，则为true\n\tSize              int64         `json:\"size\"`               //\n\tState             TorrentStatus `json:\"state\"`              // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list\n\tSuperSeeding      bool          `json:\"super_seeding\"`      // 如果启用超级播种，则为true\n\tTags              string        `json:\"tags\"`               // Torrent 的逗号连接标签列表\n\tTimeActive        int           `json:\"time_active\"`        // 总活动时间（秒）\n\tTotalSize         int64         `json:\"total_size\"`         // 此 torrent 中所有文件的总大小（字节）（包括未选择的文件）\n\tTracker           string        `json:\"tracker\"`            // 第一个具有工作状态的tracker。如果没有tracker在工作，则返回空字符串。\n\tTrackersCount     int           `json:\"trackers_count\"`     //\n\tUpLimit           int           `json:\"up_limit\"`           // 上传限制\n\tUploaded          int64         `json:\"uploaded\"`           // 累计上传\n\tUploadedSession   int64         `json:\"uploaded_session\"`   // 当前session累计上传\n\tUpspeed           int           `json:\"upspeed\"`            // 上传速度（字节/秒）\n}\n\ntype InfoNotFoundError struct {\n\tId  string\n\tErr error\n}\n\nfunc (i InfoNotFoundError) Error() string {\n\treturn \"there should be exactly one task with tag \\\"openlist-\" + i.Id + \"\\\"\"\n}\n\nfunc NewInfoNotFoundError(id string) InfoNotFoundError {\n\treturn InfoNotFoundError{Id: id}\n}\n\nfunc (c *client) GetInfo(id string) (TorrentInfo, error) {\n\tvar infos []TorrentInfo\n\n\terr := c.checkAuthorization()\n\tif err != nil {\n\t\treturn TorrentInfo{}, err\n\t}\n\n\tv := url.Values{}\n\tv.Set(\"tag\", \"openlist-\"+id)\n\tresponse, err := c.post(\"/api/v2/torrents/info\", v)\n\tif err != nil {\n\t\treturn TorrentInfo{}, err\n\t}\n\tdefer response.Body.Close()\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn TorrentInfo{}, err\n\t}\n\terr = utils.Json.Unmarshal(body, &infos)\n\tif err != nil {\n\t\treturn TorrentInfo{}, err\n\t}\n\tif len(infos) != 1 {\n\t\treturn TorrentInfo{}, NewInfoNotFoundError(id)\n\t}\n\treturn infos[0], nil\n}\n\ntype FileInfo struct {\n\tIndex        int     `json:\"index\"`\n\tName         string  `json:\"name\"`\n\tSize         int64   `json:\"size\"`\n\tProgress     float32 `json:\"progress\"`\n\tPriority     int     `json:\"priority\"`\n\tIsSeed       bool    `json:\"is_seed\"`\n\tPieceRange   []int   `json:\"piece_range\"`\n\tAvailability float32 `json:\"availability\"`\n}\n\nfunc (c *client) GetFiles(id string) ([]FileInfo, error) {\n\tvar infos []FileInfo\n\n\terr := c.checkAuthorization()\n\tif err != nil {\n\t\treturn []FileInfo{}, err\n\t}\n\n\ttInfo, err := c.GetInfo(id)\n\tif err != nil {\n\t\treturn []FileInfo{}, err\n\t}\n\n\tv := url.Values{}\n\tv.Set(\"hash\", tInfo.Hash)\n\tresponse, err := c.post(\"/api/v2/torrents/files\", v)\n\tif err != nil {\n\t\treturn []FileInfo{}, err\n\t}\n\tdefer response.Body.Close()\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn []FileInfo{}, err\n\t}\n\terr = utils.Json.Unmarshal(body, &infos)\n\tif err != nil {\n\t\treturn []FileInfo{}, err\n\t}\n\treturn infos, nil\n}\n\nfunc (c *client) Delete(id string, deleteFiles bool) error {\n\terr := c.checkAuthorization()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinfo, err := c.GetInfo(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tv := url.Values{}\n\tv.Set(\"hashes\", info.Hash)\n\tif deleteFiles {\n\t\tv.Set(\"deleteFiles\", \"true\")\n\t} else {\n\t\tv.Set(\"deleteFiles\", \"false\")\n\t}\n\tdeleteResp, err := c.post(\"/api/v2/torrents/delete\", v)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer deleteResp.Body.Close()\n\tif deleteResp.StatusCode != 200 {\n\t\treturn errors.New(\"failed to delete qbittorrent task\")\n\t}\n\n\tv = url.Values{}\n\tv.Set(\"tags\", \"openlist-\"+id)\n\tdeleteTagsResp, err := c.post(\"/api/v2/torrents/deleteTags\", v)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer deleteTagsResp.Body.Close()\n\tif deleteTagsResp.StatusCode != 200 {\n\t\treturn errors.New(\"failed to delete qbittorrent tag\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sign/hmac.go",
    "content": "package sign\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype HMACSign struct {\n\tSecretKey []byte\n}\n\nfunc (s HMACSign) Sign(data string, expire int64) string {\n\th := hmac.New(sha256.New, s.SecretKey)\n\texpireTimeStamp := strconv.FormatInt(expire, 10)\n\t_, err := io.WriteString(h, data+\":\"+expireTimeStamp)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn base64.URLEncoding.EncodeToString(h.Sum(nil)) + \":\" + expireTimeStamp\n}\n\nfunc (s HMACSign) Verify(data, sign string) error {\n\tsignSlice := strings.Split(sign, \":\")\n\t// check whether contains expire time\n\tif signSlice[len(signSlice)-1] == \"\" {\n\t\treturn ErrExpireMissing\n\t}\n\t// check whether expire time is expired\n\texpires, err := strconv.ParseInt(signSlice[len(signSlice)-1], 10, 64)\n\tif err != nil {\n\t\treturn ErrExpireInvalid\n\t}\n\t// if expire time is expired, return error\n\tif expires < time.Now().Unix() && expires != 0 {\n\t\treturn ErrSignExpired\n\t}\n\t// verify sign\n\tif s.Sign(data, expires) != sign {\n\t\treturn ErrSignInvalid\n\t}\n\treturn nil\n}\n\nfunc NewHMACSign(secret []byte) Sign {\n\treturn HMACSign{SecretKey: secret}\n}\n"
  },
  {
    "path": "pkg/sign/sign.go",
    "content": "package sign\n\nimport \"errors\"\n\ntype Sign interface {\n\tSign(data string, expire int64) string\n\tVerify(data, sign string) error\n}\n\nvar (\n\tErrSignExpired   = errors.New(\"sign expired\")\n\tErrSignInvalid   = errors.New(\"sign invalid\")\n\tErrExpireInvalid = errors.New(\"expire invalid\")\n\tErrExpireMissing = errors.New(\"expire missing\")\n)\n"
  },
  {
    "path": "pkg/singleflight/signleflight_test.go",
    "content": "// Copyright 2013 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage singleflight // import \"golang.org/x/sync/singleflight\"\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype errValue struct{}\n\nfunc (err *errValue) Error() string {\n\treturn \"error value\"\n}\n\nfunc TestPanicErrorUnwrap(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname             string\n\t\tpanicValue       any\n\t\twrappedErrorType bool\n\t}{\n\t\t{\n\t\t\tname:             \"panicError wraps non-error type\",\n\t\t\tpanicValue:       &panicError{value: \"string value\"},\n\t\t\twrappedErrorType: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"panicError wraps error type\",\n\t\t\tpanicValue:       &panicError{value: new(errValue)},\n\t\t\twrappedErrorType: false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar recovered any\n\n\t\t\tgroup := &Group[any]{}\n\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\trecovered = recover()\n\t\t\t\t\tt.Logf(\"after panic(%#v) in group.Do, recovered %#v\", tc.panicValue, recovered)\n\t\t\t\t}()\n\n\t\t\t\t_, _, _ = group.Do(tc.name, func() (any, error) {\n\t\t\t\t\tpanic(tc.panicValue)\n\t\t\t\t})\n\t\t\t}()\n\n\t\t\tif recovered == nil {\n\t\t\t\tt.Fatal(\"expected a non-nil panic value\")\n\t\t\t}\n\n\t\t\terr, ok := recovered.(error)\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"recovered non-error type: %T\", recovered)\n\t\t\t}\n\n\t\t\tif !errors.Is(err, new(errValue)) && tc.wrappedErrorType {\n\t\t\t\tt.Errorf(\"unexpected wrapped error type %T; want %T\", err, new(errValue))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDo(t *testing.T) {\n\tvar g Group[string]\n\tv, err, _ := g.Do(\"key\", func() (string, error) {\n\t\treturn \"bar\", nil\n\t})\n\tif got, want := fmt.Sprintf(\"%v (%T)\", v, v), \"bar (string)\"; got != want {\n\t\tt.Errorf(\"Do = %v; want %v\", got, want)\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"Do error = %v\", err)\n\t}\n}\n\nfunc TestDoErr(t *testing.T) {\n\tvar g Group[any]\n\tsomeErr := errors.New(\"Some error\")\n\tv, err, _ := g.Do(\"key\", func() (any, error) {\n\t\treturn nil, someErr\n\t})\n\tif err != someErr {\n\t\tt.Errorf(\"Do error = %v; want someErr %v\", err, someErr)\n\t}\n\tif v != nil {\n\t\tt.Errorf(\"unexpected non-nil value %#v\", v)\n\t}\n}\n\nfunc TestDoDupSuppress(t *testing.T) {\n\tvar g Group[string]\n\tvar wg1, wg2 sync.WaitGroup\n\tc := make(chan string, 1)\n\tvar calls int32\n\tfn := func() (string, error) {\n\t\tif atomic.AddInt32(&calls, 1) == 1 {\n\t\t\t// First invocation.\n\t\t\twg1.Done()\n\t\t}\n\t\tv := <-c\n\t\tc <- v // pump; make available for any future calls\n\n\t\ttime.Sleep(10 * time.Millisecond) // let more goroutines enter Do\n\n\t\treturn v, nil\n\t}\n\n\tconst n = 10\n\twg1.Add(1)\n\tfor i := 0; i < n; i++ {\n\t\twg1.Add(1)\n\t\twg2.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg2.Done()\n\t\t\twg1.Done()\n\t\t\tv, err, _ := g.Do(\"key\", fn)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Do error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif v != \"bar\" {\n\t\t\t\tt.Errorf(\"Do = %T %v; want %q\", v, v, \"bar\")\n\t\t\t}\n\t\t}()\n\t}\n\twg1.Wait()\n\t// At least one goroutine is in fn now and all of them have at\n\t// least reached the line before the Do.\n\tc <- \"bar\"\n\twg2.Wait()\n\tif got := atomic.LoadInt32(&calls); got <= 0 || got >= n {\n\t\tt.Errorf(\"number of calls = %d; want over 0 and less than %d\", got, n)\n\t}\n}\n\n// Test that singleflight behaves correctly after Forget called.\n// See https://github.com/golang/go/issues/31420\nfunc TestForget(t *testing.T) {\n\tvar g Group[int]\n\n\tvar (\n\t\tfirstStarted  = make(chan struct{})\n\t\tunblockFirst  = make(chan struct{})\n\t\tfirstFinished = make(chan struct{})\n\t)\n\n\tgo func() {\n\t\tg.Do(\"key\", func() (i int, e error) {\n\t\t\tclose(firstStarted)\n\t\t\t<-unblockFirst\n\t\t\tclose(firstFinished)\n\t\t\treturn\n\t\t})\n\t}()\n\t<-firstStarted\n\tg.Forget(\"key\")\n\n\tunblockSecond := make(chan struct{})\n\tsecondResult := g.DoChan(\"key\", func() (i int, e error) {\n\t\t<-unblockSecond\n\t\treturn 2, nil\n\t})\n\n\tclose(unblockFirst)\n\t<-firstFinished\n\n\tthirdResult := g.DoChan(\"key\", func() (i int, e error) {\n\t\treturn 3, nil\n\t})\n\n\tclose(unblockSecond)\n\t<-secondResult\n\tr := <-thirdResult\n\tif r.Val != 2 {\n\t\tt.Errorf(\"We should receive result produced by second call, expected: 2, got %d\", r.Val)\n\t}\n}\n\nfunc TestDoChan(t *testing.T) {\n\tvar g Group[string]\n\tch := g.DoChan(\"key\", func() (string, error) {\n\t\treturn \"bar\", nil\n\t})\n\n\tres := <-ch\n\tv := res.Val\n\terr := res.Err\n\tif got, want := fmt.Sprintf(\"%v (%T)\", v, v), \"bar (string)\"; got != want {\n\t\tt.Errorf(\"Do = %v; want %v\", got, want)\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"Do error = %v\", err)\n\t}\n}\n\n// Test singleflight behaves correctly after Do panic.\n// See https://github.com/golang/go/issues/41133\nfunc TestPanicDo(t *testing.T) {\n\tvar g Group[any]\n\tfn := func() (any, error) {\n\t\tpanic(\"invalid memory address or nil pointer dereference\")\n\t}\n\n\tconst n = 5\n\twaited := int32(n)\n\tpanicCount := int32(0)\n\tdone := make(chan struct{})\n\tfor i := 0; i < n; i++ {\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif err := recover(); err != nil {\n\t\t\t\t\tt.Logf(\"Got panic: %v\\n%s\", err, debug.Stack())\n\t\t\t\t\tatomic.AddInt32(&panicCount, 1)\n\t\t\t\t}\n\n\t\t\t\tif atomic.AddInt32(&waited, -1) == 0 {\n\t\t\t\t\tclose(done)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tg.Do(\"key\", fn)\n\t\t}()\n\t}\n\n\tselect {\n\tcase <-done:\n\t\tif panicCount != n {\n\t\t\tt.Errorf(\"Expect %d panic, but got %d\", n, panicCount)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatalf(\"Do hangs\")\n\t}\n}\n\nfunc TestGoexitDo(t *testing.T) {\n\tvar g Group[any]\n\tfn := func() (any, error) {\n\t\truntime.Goexit()\n\t\treturn nil, nil\n\t}\n\n\tconst n = 5\n\twaited := int32(n)\n\tdone := make(chan struct{})\n\tfor i := 0; i < n; i++ {\n\t\tgo func() {\n\t\t\tvar err error\n\t\t\tdefer func() {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Error should be nil, but got: %v\", err)\n\t\t\t\t}\n\t\t\t\tif atomic.AddInt32(&waited, -1) == 0 {\n\t\t\t\t\tclose(done)\n\t\t\t\t}\n\t\t\t}()\n\t\t\t_, err, _ = g.Do(\"key\", fn)\n\t\t}()\n\t}\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatalf(\"Do hangs\")\n\t}\n}\n\nfunc executable(t testing.TB) string {\n\texe, err := os.Executable()\n\tif err != nil {\n\t\tt.Skipf(\"skipping: test executable not found\")\n\t}\n\n\t// Control case: check whether exec.Command works at all.\n\t// (For example, it might fail with a permission error on iOS.)\n\tcmd := exec.Command(exe, \"-test.list=^$\")\n\tcmd.Env = []string{}\n\tif err := cmd.Run(); err != nil {\n\t\tt.Skipf(\"skipping: exec appears not to work on %s: %v\", runtime.GOOS, err)\n\t}\n\n\treturn exe\n}\n\nfunc TestPanicDoChan(t *testing.T) {\n\tif os.Getenv(\"TEST_PANIC_DOCHAN\") != \"\" {\n\t\tdefer func() {\n\t\t\trecover()\n\t\t}()\n\n\t\tg := new(Group[any])\n\t\tch := g.DoChan(\"\", func() (any, error) {\n\t\t\tpanic(\"Panicking in DoChan\")\n\t\t})\n\t\t<-ch\n\t\tt.Fatalf(\"DoChan unexpectedly returned\")\n\t}\n\n\tt.Parallel()\n\n\tcmd := exec.Command(executable(t), \"-test.run=\"+t.Name(), \"-test.v\")\n\tcmd.Env = append(os.Environ(), \"TEST_PANIC_DOCHAN=1\")\n\tout := new(bytes.Buffer)\n\tcmd.Stdout = out\n\tcmd.Stderr = out\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr := cmd.Wait()\n\tt.Logf(\"%s:\\n%s\", strings.Join(cmd.Args, \" \"), out)\n\tif err == nil {\n\t\tt.Errorf(\"Test subprocess passed; want a crash due to panic in DoChan\")\n\t}\n\tif bytes.Contains(out.Bytes(), []byte(\"DoChan unexpectedly\")) {\n\t\tt.Errorf(\"Test subprocess failed with an unexpected failure mode.\")\n\t}\n\tif !bytes.Contains(out.Bytes(), []byte(\"Panicking in DoChan\")) {\n\t\tt.Errorf(\"Test subprocess failed, but the crash isn't caused by panicking in DoChan\")\n\t}\n}\n\nfunc TestPanicDoSharedByDoChan(t *testing.T) {\n\tif os.Getenv(\"TEST_PANIC_DOCHAN\") != \"\" {\n\t\tblocked := make(chan struct{})\n\t\tunblock := make(chan struct{})\n\n\t\tg := new(Group[any])\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\trecover()\n\t\t\t}()\n\t\t\tg.Do(\"\", func() (any, error) {\n\t\t\t\tclose(blocked)\n\t\t\t\t<-unblock\n\t\t\t\tpanic(\"Panicking in Do\")\n\t\t\t})\n\t\t}()\n\n\t\t<-blocked\n\t\tch := g.DoChan(\"\", func() (any, error) {\n\t\t\tpanic(\"DoChan unexpectedly executed callback\")\n\t\t})\n\t\tclose(unblock)\n\t\t<-ch\n\t\tt.Fatalf(\"DoChan unexpectedly returned\")\n\t}\n\n\tt.Parallel()\n\n\tcmd := exec.Command(executable(t), \"-test.run=\"+t.Name(), \"-test.v\")\n\tcmd.Env = append(os.Environ(), \"TEST_PANIC_DOCHAN=1\")\n\tout := new(bytes.Buffer)\n\tcmd.Stdout = out\n\tcmd.Stderr = out\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr := cmd.Wait()\n\tt.Logf(\"%s:\\n%s\", strings.Join(cmd.Args, \" \"), out)\n\tif err == nil {\n\t\tt.Errorf(\"Test subprocess passed; want a crash due to panic in Do shared by DoChan\")\n\t}\n\tif bytes.Contains(out.Bytes(), []byte(\"DoChan unexpectedly\")) {\n\t\tt.Errorf(\"Test subprocess failed with an unexpected failure mode.\")\n\t}\n\tif !bytes.Contains(out.Bytes(), []byte(\"Panicking in Do\")) {\n\t\tt.Errorf(\"Test subprocess failed, but the crash isn't caused by panicking in Do\")\n\t}\n}\n\nfunc ExampleGroup() {\n\tg := new(Group[string])\n\n\tblock := make(chan struct{})\n\tres1c := g.DoChan(\"key\", func() (string, error) {\n\t\t<-block\n\t\treturn \"func 1\", nil\n\t})\n\tres2c := g.DoChan(\"key\", func() (string, error) {\n\t\t<-block\n\t\treturn \"func 2\", nil\n\t})\n\tclose(block)\n\n\tres1 := <-res1c\n\tres2 := <-res2c\n\n\t// Results are shared by functions executed with duplicate keys.\n\tfmt.Println(\"Shared:\", res2.Shared)\n\t// Only the first function is executed: it is registered and started with \"key\",\n\t// and doesn't complete before the second function is registered with a duplicate key.\n\tfmt.Println(\"Equal results:\", res1.Val == res2.Val)\n\tfmt.Println(\"Result:\", res1.Val)\n\n\t// Output:\n\t// Shared: true\n\t// Equal results: true\n\t// Result: func 1\n}\n"
  },
  {
    "path": "pkg/singleflight/singleflight.go",
    "content": "// Copyright 2013 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package singleflight provides a duplicate function call suppression\n// mechanism.\npackage singleflight // import \"golang.org/x/sync/singleflight\"\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sync\"\n)\n\n// errGoexit indicates the runtime.Goexit was called in\n// the user given function.\nvar errGoexit = errors.New(\"runtime.Goexit was called\")\n\n// A panicError is an arbitrary value recovered from a panic\n// with the stack trace during the execution of given function.\ntype panicError struct {\n\tvalue any\n\tstack []byte\n}\n\n// Error implements error interface.\nfunc (p *panicError) Error() string {\n\treturn fmt.Sprintf(\"%v\\n\\n%s\", p.value, p.stack)\n}\n\nfunc (p *panicError) Unwrap() error {\n\terr, ok := p.value.(error)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc newPanicError(v any) error {\n\tstack := debug.Stack()\n\n\t// The first line of the stack trace is of the form \"goroutine N [status]:\"\n\t// but by the time the panic reaches Do the goroutine may no longer exist\n\t// and its status will have changed. Trim out the misleading line.\n\tif line := bytes.IndexByte(stack[:], '\\n'); line >= 0 {\n\t\tstack = stack[line+1:]\n\t}\n\treturn &panicError{value: v, stack: stack}\n}\n\n// call is an in-flight or completed singleflight.Do call\ntype call[T any] struct {\n\twg sync.WaitGroup\n\n\t// These fields are written once before the WaitGroup is done\n\t// and are only read after the WaitGroup is done.\n\tval T\n\terr error\n\n\t// These fields are read and written with the singleflight\n\t// mutex held before the WaitGroup is done, and are read but\n\t// not written after the WaitGroup is done.\n\tdups  int\n\tchans []chan<- Result[T]\n}\n\n// Group represents a class of work and forms a namespace in\n// which units of work can be executed with duplicate suppression.\ntype Group[T any] struct {\n\tmu sync.Mutex          // protects m\n\tm  map[string]*call[T] // lazily initialized\n}\n\n// Result holds the results of Do, so they can be passed\n// on a channel.\ntype Result[T any] struct {\n\tVal    T\n\tErr    error\n\tShared bool\n}\n\n// Do executes and returns the results of the given function, making\n// sure that only one execution is in-flight for a given key at a\n// time. If a duplicate comes in, the duplicate caller waits for the\n// original to complete and receives the same results.\n// The return value shared indicates whether v was given to multiple callers.\nfunc (g *Group[T]) Do(key string, fn func() (T, error)) (v T, err error, shared bool) {\n\tg.mu.Lock()\n\tif g.m == nil {\n\t\tg.m = make(map[string]*call[T])\n\t}\n\tif c, ok := g.m[key]; ok {\n\t\tc.dups++\n\t\tg.mu.Unlock()\n\t\tc.wg.Wait()\n\n\t\tif e, ok := c.err.(*panicError); ok {\n\t\t\tpanic(e)\n\t\t} else if c.err == errGoexit {\n\t\t\truntime.Goexit()\n\t\t}\n\t\treturn c.val, c.err, true\n\t}\n\tc := new(call[T])\n\tc.wg.Add(1)\n\tg.m[key] = c\n\tg.mu.Unlock()\n\n\tg.doCall(c, key, fn)\n\treturn c.val, c.err, c.dups > 0\n}\n\n// DoChan is like Do but returns a channel that will receive the\n// results when they are ready.\n//\n// The returned channel will not be closed.\nfunc (g *Group[T]) DoChan(key string, fn func() (T, error)) <-chan Result[T] {\n\tch := make(chan Result[T], 1)\n\tg.mu.Lock()\n\tif g.m == nil {\n\t\tg.m = make(map[string]*call[T])\n\t}\n\tif c, ok := g.m[key]; ok {\n\t\tc.dups++\n\t\tc.chans = append(c.chans, ch)\n\t\tg.mu.Unlock()\n\t\treturn ch\n\t}\n\tc := &call[T]{chans: []chan<- Result[T]{ch}}\n\tc.wg.Add(1)\n\tg.m[key] = c\n\tg.mu.Unlock()\n\n\tgo g.doCall(c, key, fn)\n\n\treturn ch\n}\n\n// doCall handles the single call for a key.\nfunc (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) {\n\tnormalReturn := false\n\trecovered := false\n\n\t// use double-defer to distinguish panic from runtime.Goexit,\n\t// more details see https://golang.org/cl/134395\n\tdefer func() {\n\t\t// the given function invoked runtime.Goexit\n\t\tif !normalReturn && !recovered {\n\t\t\tc.err = errGoexit\n\t\t}\n\n\t\tg.mu.Lock()\n\t\tdefer g.mu.Unlock()\n\t\tc.wg.Done()\n\t\tif g.m[key] == c {\n\t\t\tdelete(g.m, key)\n\t\t}\n\n\t\tif e, ok := c.err.(*panicError); ok {\n\t\t\t// In order to prevent the waiting channels from being blocked forever,\n\t\t\t// needs to ensure that this panic cannot be recovered.\n\t\t\tif len(c.chans) > 0 {\n\t\t\t\tgo panic(e)\n\t\t\t\tselect {} // Keep this goroutine around so that it will appear in the crash dump.\n\t\t\t} else {\n\t\t\t\tpanic(e)\n\t\t\t}\n\t\t} else if c.err == errGoexit {\n\t\t\t// Already in the process of goexit, no need to call again\n\t\t} else {\n\t\t\t// Normal return\n\t\t\tfor _, ch := range c.chans {\n\t\t\t\tch <- Result[T]{c.val, c.err, c.dups > 0}\n\t\t\t}\n\t\t}\n\t}()\n\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif !normalReturn {\n\t\t\t\t// Ideally, we would wait to take a stack trace until we've determined\n\t\t\t\t// whether this is a panic or a runtime.Goexit.\n\t\t\t\t//\n\t\t\t\t// Unfortunately, the only way we can distinguish the two is to see\n\t\t\t\t// whether the recover stopped the goroutine from terminating, and by\n\t\t\t\t// the time we know that, the part of the stack trace relevant to the\n\t\t\t\t// panic has been discarded.\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tc.err = newPanicError(r)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tc.val, c.err = fn()\n\t\tnormalReturn = true\n\t}()\n\n\tif !normalReturn {\n\t\trecovered = true\n\t}\n}\n\n// Forget tells the singleflight to forget about a key.  Future calls\n// to Do for this key will call the function rather than waiting for\n// an earlier call to complete.\nfunc (g *Group[T]) Forget(key string) {\n\tg.mu.Lock()\n\tdelete(g.m, key)\n\tg.mu.Unlock()\n}\n"
  },
  {
    "path": "pkg/singleflight/var.go",
    "content": "package singleflight\n\nvar AnyGroup Group[any]\n"
  },
  {
    "path": "pkg/task/errors.go",
    "content": "package task\n\nimport \"errors\"\n\nvar (\n\tErrTaskNotFound = errors.New(\"task not found\")\n\tErrTaskRunning  = errors.New(\"task is running\")\n)\n"
  },
  {
    "path": "pkg/task/manager.go",
    "content": "package task\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Manager[K comparable] struct {\n\tcurID    K\n\tworkerC  chan struct{}\n\tupdateID func(*K)\n\ttasks    generic_sync.MapOf[K, *Task[K]]\n}\n\nfunc (tm *Manager[K]) Submit(task *Task[K]) K {\n\tif tm.updateID != nil {\n\t\ttm.updateID(&tm.curID)\n\t\ttask.ID = tm.curID\n\t}\n\ttm.tasks.Store(task.ID, task)\n\ttm.do(task)\n\treturn task.ID\n}\n\nfunc (tm *Manager[K]) do(task *Task[K]) {\n\tgo func() {\n\t\tlog.Debugf(\"task [%s] waiting for worker\", task.Name)\n\t\tselect {\n\t\tcase <-tm.workerC:\n\t\t\tlog.Debugf(\"task [%s] starting\", task.Name)\n\t\t\ttask.run()\n\t\t\tlog.Debugf(\"task [%s] ended\", task.Name)\n\t\tcase <-task.Ctx.Done():\n\t\t\tlog.Debugf(\"task [%s] canceled\", task.Name)\n\t\t\treturn\n\t\t}\n\t\t// return worker\n\t\ttm.workerC <- struct{}{}\n\t}()\n}\n\nfunc (tm *Manager[K]) GetAll() []*Task[K] {\n\treturn tm.tasks.Values()\n}\n\nfunc (tm *Manager[K]) Get(tid K) (*Task[K], bool) {\n\treturn tm.tasks.Load(tid)\n}\n\nfunc (tm *Manager[K]) MustGet(tid K) *Task[K] {\n\ttask, _ := tm.Get(tid)\n\treturn task\n}\n\nfunc (tm *Manager[K]) Retry(tid K) error {\n\tt, ok := tm.Get(tid)\n\tif !ok {\n\t\treturn errors.WithStack(ErrTaskNotFound)\n\t}\n\ttm.do(t)\n\treturn nil\n}\n\nfunc (tm *Manager[K]) Cancel(tid K) error {\n\tt, ok := tm.Get(tid)\n\tif !ok {\n\t\treturn errors.WithStack(ErrTaskNotFound)\n\t}\n\tt.Cancel()\n\treturn nil\n}\n\nfunc (tm *Manager[K]) Remove(tid K) error {\n\tt, ok := tm.Get(tid)\n\tif !ok {\n\t\treturn errors.WithStack(ErrTaskNotFound)\n\t}\n\tif !t.Done() {\n\t\treturn errors.WithStack(ErrTaskRunning)\n\t}\n\ttm.tasks.Delete(tid)\n\treturn nil\n}\n\n// RemoveAll removes all tasks from the manager, this maybe shouldn't be used\n// because the task maybe still running.\nfunc (tm *Manager[K]) RemoveAll() {\n\ttm.tasks.Clear()\n}\n\nfunc (tm *Manager[K]) RemoveByStates(states ...string) {\n\ttasks := tm.GetAll()\n\tfor _, task := range tasks {\n\t\tif utils.SliceContains(states, task.GetState()) {\n\t\t\t_ = tm.Remove(task.ID)\n\t\t}\n\t}\n}\n\nfunc (tm *Manager[K]) GetByStates(states ...string) []*Task[K] {\n\tvar tasks []*Task[K]\n\ttm.tasks.Range(func(key K, value *Task[K]) bool {\n\t\tif utils.SliceContains(states, value.GetState()) {\n\t\t\ttasks = append(tasks, value)\n\t\t}\n\t\treturn true\n\t})\n\treturn tasks\n}\n\nfunc (tm *Manager[K]) ListUndone() []*Task[K] {\n\treturn tm.GetByStates(PENDING, RUNNING, CANCELING)\n}\n\nfunc (tm *Manager[K]) ListDone() []*Task[K] {\n\treturn tm.GetByStates(SUCCEEDED, CANCELED, ERRORED)\n}\n\nfunc (tm *Manager[K]) ClearDone() {\n\ttm.RemoveByStates(SUCCEEDED, CANCELED, ERRORED)\n}\n\nfunc (tm *Manager[K]) ClearSucceeded() {\n\ttm.RemoveByStates(SUCCEEDED)\n}\n\nfunc (tm *Manager[K]) RawTasks() *generic_sync.MapOf[K, *Task[K]] {\n\treturn &tm.tasks\n}\n\nfunc NewTaskManager[K comparable](maxWorker int, updateID ...func(*K)) *Manager[K] {\n\ttm := &Manager[K]{\n\t\ttasks:   generic_sync.MapOf[K, *Task[K]]{},\n\t\tworkerC: make(chan struct{}, maxWorker),\n\t}\n\tfor i := 0; i < maxWorker; i++ {\n\t\ttm.workerC <- struct{}{}\n\t}\n\tif len(updateID) > 0 {\n\t\ttm.updateID = updateID[0]\n\t}\n\treturn tm\n}\n"
  },
  {
    "path": "pkg/task/task.go",
    "content": "// Package task manage task, such as file upload, file copy between storages, offline download, etc.\npackage task\n\nimport (\n\t\"context\"\n\t\"runtime\"\n\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tPENDING   = \"pending\"\n\tRUNNING   = \"running\"\n\tSUCCEEDED = \"succeeded\"\n\tCANCELING = \"canceling\"\n\tCANCELED  = \"canceled\"\n\tERRORED   = \"errored\"\n)\n\ntype Func[K comparable] func(task *Task[K]) error\ntype Callback[K comparable] func(task *Task[K])\n\ntype Task[K comparable] struct {\n\tID       K\n\tName     string\n\tstate    string // pending, running, finished, canceling, canceled, errored\n\tstatus   string\n\tprogress float64\n\n\tError error\n\n\tFunc     Func[K]\n\tcallback Callback[K]\n\n\tCtx    context.Context\n\tcancel context.CancelFunc\n}\n\nfunc (t *Task[K]) SetStatus(status string) {\n\tt.status = status\n}\n\nfunc (t *Task[K]) SetProgress(percentage float64) {\n\tt.progress = percentage\n}\n\nfunc (t Task[K]) GetProgress() float64 {\n\treturn t.progress\n}\n\nfunc (t Task[K]) GetState() string {\n\treturn t.state\n}\n\nfunc (t Task[K]) GetStatus() string {\n\treturn t.status\n}\n\nfunc (t Task[K]) GetErrMsg() string {\n\tif t.Error == nil {\n\t\treturn \"\"\n\t}\n\treturn t.Error.Error()\n}\n\nfunc getCurrentGoroutineStack() string {\n\tbuf := make([]byte, 1<<16)\n\tn := runtime.Stack(buf, false)\n\treturn string(buf[:n])\n}\n\nfunc (t *Task[K]) run() {\n\tt.state = RUNNING\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlog.Errorf(\"error [%s] while run task [%s],stack trace:\\n%s\", err, t.Name, getCurrentGoroutineStack())\n\t\t\tt.Error = errors.Errorf(\"panic: %+v\", err)\n\t\t\tt.state = ERRORED\n\t\t}\n\t}()\n\tt.Error = t.Func(t)\n\tif t.Error != nil {\n\t\tlog.Errorf(\"error [%+v] while run task [%s]\", t.Error, t.Name)\n\t}\n\tif errors.Is(t.Ctx.Err(), context.Canceled) {\n\t\tt.state = CANCELED\n\t} else if t.Error != nil {\n\t\tt.state = ERRORED\n\t} else {\n\t\tt.state = SUCCEEDED\n\t\tt.SetProgress(100)\n\t\tif t.callback != nil {\n\t\t\tt.callback(t)\n\t\t}\n\t}\n}\n\nfunc (t *Task[K]) retry() {\n\tt.run()\n}\n\nfunc (t *Task[K]) Done() bool {\n\treturn t.state == SUCCEEDED || t.state == CANCELED || t.state == ERRORED\n}\n\nfunc (t *Task[K]) Cancel() {\n\tif t.state == SUCCEEDED || t.state == CANCELED {\n\t\treturn\n\t}\n\tif t.cancel != nil {\n\t\tt.cancel()\n\t}\n\t// maybe can't cancel\n\tt.state = CANCELING\n}\n\nfunc WithCancelCtx[K comparable](task *Task[K]) *Task[K] {\n\tctx, cancel := context.WithCancel(context.Background())\n\ttask.Ctx = ctx\n\ttask.cancel = cancel\n\ttask.state = PENDING\n\treturn task\n}\n"
  },
  {
    "path": "pkg/task/task_test.go",
    "content": "package task\n\nimport (\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc TestTask_Manager(t *testing.T) {\n\ttm := NewTaskManager(3, func(id *uint64) {\n\t\tatomic.AddUint64(id, 1)\n\t})\n\tid := tm.Submit(WithCancelCtx(&Task[uint64]{\n\t\tName: \"test\",\n\t\tFunc: func(task *Task[uint64]) error {\n\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\treturn nil\n\t\t},\n\t}))\n\ttask, ok := tm.Get(id)\n\tif !ok {\n\t\tt.Fatal(\"task not found\")\n\t}\n\ttime.Sleep(time.Millisecond * 100)\n\tif task.state != RUNNING {\n\t\tt.Errorf(\"task status not running: %s\", task.state)\n\t}\n\ttime.Sleep(time.Second)\n\tif task.state != SUCCEEDED {\n\t\tt.Errorf(\"task status not finished: %s\", task.state)\n\t}\n}\n\nfunc TestTask_Cancel(t *testing.T) {\n\ttm := NewTaskManager(3, func(id *uint64) {\n\t\tatomic.AddUint64(id, 1)\n\t})\n\tid := tm.Submit(WithCancelCtx(&Task[uint64]{\n\t\tName: \"test\",\n\t\tFunc: func(task *Task[uint64]) error {\n\t\t\tfor {\n\t\t\t\tif utils.IsCanceled(task.Ctx) {\n\t\t\t\t\treturn nil\n\t\t\t\t} else {\n\t\t\t\t\tt.Logf(\"task is running\")\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}))\n\ttask, ok := tm.Get(id)\n\tif !ok {\n\t\tt.Fatal(\"task not found\")\n\t}\n\ttime.Sleep(time.Microsecond * 50)\n\ttask.Cancel()\n\ttime.Sleep(time.Millisecond)\n\tif task.state != CANCELED {\n\t\tt.Errorf(\"task status not canceled: %s\", task.state)\n\t}\n}\n\nfunc TestTask_Retry(t *testing.T) {\n\ttm := NewTaskManager(3, func(id *uint64) {\n\t\tatomic.AddUint64(id, 1)\n\t})\n\tnum := 0\n\tid := tm.Submit(WithCancelCtx(&Task[uint64]{\n\t\tName: \"test\",\n\t\tFunc: func(task *Task[uint64]) error {\n\t\t\tnum++\n\t\t\tif num&1 == 1 {\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}))\n\ttask, ok := tm.Get(id)\n\tif !ok {\n\t\tt.Fatal(\"task not found\")\n\t}\n\ttime.Sleep(time.Millisecond)\n\tif task.Error == nil {\n\t\tt.Error(task.state)\n\t\tt.Fatal(\"task error is nil, but expected error\")\n\t} else {\n\t\tt.Logf(\"task error: %s\", task.Error)\n\t}\n\ttask.retry()\n\ttime.Sleep(time.Millisecond)\n\tif task.Error != nil {\n\t\tt.Errorf(\"task error: %+v, but expected nil\", task.Error)\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/balance.go",
    "content": "package utils\n\nimport \"strings\"\n\nvar balance = \".balance\"\n\nfunc IsBalance(str string) bool {\n\treturn strings.Contains(str, balance)\n}\n\n// GetActualMountPath remove balance suffix\nfunc GetActualMountPath(mountPath string) string {\n\tbIndex := strings.LastIndex(mountPath, \".balance\")\n\tif bIndex != -1 {\n\t\tmountPath = mountPath[:bIndex]\n\t}\n\treturn mountPath\n}\n"
  },
  {
    "path": "pkg/utils/bool.go",
    "content": "package utils\n\nfunc IsBool(bs ...bool) bool {\n\treturn len(bs) > 0 && bs[0]\n}\n"
  },
  {
    "path": "pkg/utils/ctx.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n)\n\nfunc IsCanceled(ctx context.Context) bool {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/email.go",
    "content": "package utils\n\nimport \"regexp\"\n\nfunc IsEmailFormat(email string) bool {\n\tpattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\\.){1,4}[a-z]{2,4}$`\n\treg := regexp.MustCompile(pattern)\n\treturn reg.MatchString(email)\n}\n"
  },
  {
    "path": "pkg/utils/file.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// CopyFile File copies a single file from src to dst\nfunc CopyFile(src, dst string) error {\n\tvar err error\n\tvar srcfd *os.File\n\tvar dstfd *os.File\n\tvar srcinfo os.FileInfo\n\n\tif srcfd, err = os.Open(src); err != nil {\n\t\treturn err\n\t}\n\tdefer srcfd.Close()\n\n\tif dstfd, err = CreateNestedFile(dst); err != nil {\n\t\treturn err\n\t}\n\tdefer dstfd.Close()\n\n\tif _, err = CopyWithBuffer(dstfd, srcfd); err != nil {\n\t\treturn err\n\t}\n\tif srcinfo, err = os.Stat(src); err != nil {\n\t\treturn err\n\t}\n\treturn os.Chmod(dst, srcinfo.Mode())\n}\n\n// CopyDir Dir copies a whole directory recursively\nfunc CopyDir(src, dst string) error {\n\tvar err error\n\tvar fds []os.DirEntry\n\tvar srcinfo os.FileInfo\n\n\tif srcinfo, err = os.Stat(src); err != nil {\n\t\treturn err\n\t}\n\tif err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {\n\t\treturn err\n\t}\n\tif fds, err = os.ReadDir(src); err != nil {\n\t\treturn err\n\t}\n\tfor _, fd := range fds {\n\t\tsrcfp := path.Join(src, fd.Name())\n\t\tdstfp := path.Join(dst, fd.Name())\n\n\t\tif fd.IsDir() {\n\t\t\tif err = CopyDir(srcfp, dstfp); err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err = CopyFile(srcfp, dstfp); err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// SymlinkOrCopyFile symlinks a file or copy if symlink failed\nfunc SymlinkOrCopyFile(src, dst string) error {\n\tif err := CreateNestedDirectory(filepath.Dir(dst)); err != nil {\n\t\treturn err\n\t}\n\tif err := os.Symlink(src, dst); err != nil {\n\t\treturn CopyFile(src, dst)\n\t}\n\treturn nil\n}\n\n// Exists determine whether the file exists\nfunc Exists(name string) bool {\n\tif _, err := os.Stat(name); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// CreateNestedDirectory create nested directory\nfunc CreateNestedDirectory(path string) error {\n\terr := os.MkdirAll(path, 0700)\n\tif err != nil {\n\t\tlog.Errorf(\"can't create folder, %s\", err)\n\t}\n\treturn err\n}\n\n// CreateNestedFile create nested file\nfunc CreateNestedFile(path string) (*os.File, error) {\n\tbasePath := filepath.Dir(path)\n\tif err := CreateNestedDirectory(basePath); err != nil {\n\t\treturn nil, err\n\t}\n\treturn os.Create(path)\n}\n\n// CreateTempFile create temp file from io.ReadCloser, and seek to 0\nfunc CreateTempFile(r io.Reader, size int64) (*os.File, error) {\n\tif f, ok := r.(*os.File); ok {\n\t\treturn f, nil\n\t}\n\tf, err := os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treadBytes, err := CopyWithBuffer(f, r)\n\tif err != nil {\n\t\t_ = os.Remove(f.Name())\n\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed\")\n\t}\n\tif size > 0 && readBytes != size {\n\t\t_ = os.Remove(f.Name())\n\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, incoming stream actual size= %d, expect = %d \", readBytes, size)\n\t}\n\t_, err = f.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\t_ = os.Remove(f.Name())\n\t\treturn nil, errs.NewErr(err, \"CreateTempFile failed, can't seek to 0 \")\n\t}\n\treturn f, nil\n}\n\n// GetFileType get file type\nfunc GetFileType(filename string) int {\n\text := strings.ToLower(Ext(filename))\n\tif SliceContains(conf.SlicesMap[conf.AudioTypes], ext) {\n\t\treturn conf.AUDIO\n\t}\n\tif SliceContains(conf.SlicesMap[conf.VideoTypes], ext) {\n\t\treturn conf.VIDEO\n\t}\n\tif SliceContains(conf.SlicesMap[conf.ImageTypes], ext) {\n\t\treturn conf.IMAGE\n\t}\n\tif SliceContains(conf.SlicesMap[conf.TextTypes], ext) {\n\t\treturn conf.TEXT\n\t}\n\treturn conf.UNKNOWN\n}\n\nfunc GetObjType(filename string, isDir bool) int {\n\tif isDir {\n\t\treturn conf.FOLDER\n\t}\n\treturn GetFileType(filename)\n}\n\nvar extraMimeTypes = map[string]string{\n\t\".apk\": \"application/vnd.android.package-archive\",\n}\n\nfunc GetMimeType(name string) string {\n\text := path.Ext(name)\n\tif m, ok := extraMimeTypes[ext]; ok {\n\t\treturn m\n\t}\n\tm := mime.TypeByExtension(ext)\n\tif m != \"\" {\n\t\treturn m\n\t}\n\treturn \"application/octet-stream\"\n}\n\nconst (\n\tKB = 1 << (10 * (iota + 1))\n\tMB\n\tGB\n\tTB\n)\n\n// IsSystemFile checks if a filename is a common system file that should be ignored\n// Returns true for files like .DS_Store, desktop.ini, Thumbs.db, and Apple Double files (._*)\nfunc IsSystemFile(filename string) bool {\n\t// Common system files\n\tswitch filename {\n\tcase \".DS_Store\", \"desktop.ini\", \"Thumbs.db\", \"@eaDir\":\n\t\treturn true\n\t}\n\n\t// Apple Double files (._*)\n\tif strings.HasPrefix(filename, \"._\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/utils/file_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n)\n\nfunc TestIsSystemFile(t *testing.T) {\n\ttestCases := []struct {\n\t\tfilename string\n\t\texpected bool\n\t}{\n\t\t// System files that should be filtered\n\t\t{\".DS_Store\", true},\n\t\t{\"desktop.ini\", true},\n\t\t{\"Thumbs.db\", true},\n\t\t{\"._test.txt\", true},\n\t\t{\"._\", true},\n\t\t{\"._somefile\", true},\n\t\t{\"._folder_name\", true},\n\t\t{\"@eaDir\", true},\n\n\t\t// Regular files that should not be filtered\n\t\t{\"test.txt\", false},\n\t\t{\"file.pdf\", false},\n\t\t{\"document.docx\", false},\n\t\t{\".gitignore\", false},\n\t\t{\".env\", false},\n\t\t{\"_underscore.txt\", false},\n\t\t{\"normal_file.txt\", false},\n\t\t{\"\", false},\n\t\t{\".hidden\", false},\n\t\t{\"..special\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.filename, func(t *testing.T) {\n\t\t\tresult := IsSystemFile(tc.filename)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"IsSystemFile(%q) = %v, want %v\", tc.filename, result, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/hash/gcid.go",
    "content": "package hash_extend\n\nimport (\n\t\"crypto/sha1\"\n\t\"encoding\"\n\t\"fmt\"\n\t\"hash\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nvar GCID = utils.RegisterHashWithParam(\"gcid\", \"GCID\", 40, func(a ...any) hash.Hash {\n\tvar (\n\t\tsize int64\n\t\terr  error\n\t)\n\tif len(a) > 0 {\n\t\tsize, err = strconv.ParseInt(fmt.Sprint(a[0]), 10, 64)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn NewGcid(size)\n})\n\nfunc NewGcid(size int64) hash.Hash {\n\tcalcBlockSize := func(j int64) int64 {\n\t\tvar psize int64 = 0x40000\n\t\tfor float64(j)/float64(psize) > 0x200 && psize < 0x200000 {\n\t\t\tpsize = psize << 1\n\t\t}\n\t\treturn psize\n\t}\n\n\treturn &gcid{\n\t\thash:      sha1.New(),\n\t\thashState: sha1.New(),\n\t\tblockSize: int(calcBlockSize(size)),\n\t}\n}\n\ntype gcid struct {\n\thash      hash.Hash\n\thashState hash.Hash\n\tblockSize int\n\n\toffset int\n}\n\nfunc (h *gcid) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\tfor len(p) > 0 {\n\t\tif h.offset < h.blockSize {\n\t\t\tvar lastSize = h.blockSize - h.offset\n\t\t\tif lastSize > len(p) {\n\t\t\t\tlastSize = len(p)\n\t\t\t}\n\n\t\t\th.hashState.Write(p[:lastSize])\n\t\t\th.offset += lastSize\n\t\t\tp = p[lastSize:]\n\t\t}\n\n\t\tif h.offset >= h.blockSize {\n\t\t\th.hash.Write(h.hashState.Sum(nil))\n\t\t\th.hashState.Reset()\n\t\t\th.offset = 0\n\t\t}\n\t}\n\treturn\n}\n\nfunc (h *gcid) Sum(b []byte) []byte {\n\tif h.offset != 0 {\n\t\tif hashm, ok := h.hash.(encoding.BinaryMarshaler); ok {\n\t\t\tif hashum, ok := h.hash.(encoding.BinaryUnmarshaler); ok {\n\t\t\t\ttempData, _ := hashm.MarshalBinary()\n\t\t\t\tdefer hashum.UnmarshalBinary(tempData)\n\t\t\t\th.hash.Write(h.hashState.Sum(nil))\n\t\t\t}\n\t\t}\n\t}\n\treturn h.hash.Sum(b)\n}\n\nfunc (h *gcid) Reset() {\n\th.hash.Reset()\n\th.hashState.Reset()\n}\n\nfunc (h *gcid) Size() int {\n\treturn h.hash.Size()\n}\n\nfunc (h *gcid) BlockSize() int {\n\treturn h.blockSize\n}\n"
  },
  {
    "path": "pkg/utils/hash.go",
    "content": "package utils\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"hash\"\n\t\"io\"\n\t\"iter\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc GetMD5EncodeStr(data string) string {\n\treturn HashData(MD5, []byte(data))\n}\n\n//inspired by \"github.com/rclone/rclone/fs/hash\"\n\n// ErrUnsupported should be returned by filesystem,\n// if it is requested to deliver an unsupported hash type.\nvar ErrUnsupported = errors.New(\"hash type not supported\")\n\n// HashType indicates a standard hashing algorithm\ntype HashType struct {\n\tWidth   int\n\tName    string\n\tAlias   string\n\tNewFunc func(...any) hash.Hash\n}\n\nfunc (ht *HashType) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + ht.Name + `\"`), nil\n}\n\nfunc (ht *HashType) MarshalText() (text []byte, err error) {\n\treturn []byte(ht.Name), nil\n}\n\nvar (\n\t_ json.Marshaler = (*HashType)(nil)\n\t//_ json.Unmarshaler = (*HashType)(nil)\n\n\t// read/write from/to json keys\n\t_ encoding.TextMarshaler = (*HashType)(nil)\n\t//_ encoding.TextUnmarshaler = (*HashType)(nil)\n)\n\nvar (\n\tname2hash  = map[string]*HashType{}\n\talias2hash = map[string]*HashType{}\n\tSupported  []*HashType\n)\n\nfunc GetHashByName(name string) (ht *HashType, ok bool) {\n\tht, ok = name2hash[name]\n\treturn\n}\n\n// RegisterHash adds a new Hash to the list and returns its Type\nfunc RegisterHash(name, alias string, width int, newFunc func() hash.Hash) *HashType {\n\treturn RegisterHashWithParam(name, alias, width, func(a ...any) hash.Hash { return newFunc() })\n}\n\nfunc RegisterHashWithParam(name, alias string, width int, newFunc func(...any) hash.Hash) *HashType {\n\tnewType := &HashType{\n\t\tName:    name,\n\t\tAlias:   alias,\n\t\tWidth:   width,\n\t\tNewFunc: newFunc,\n\t}\n\n\tname2hash[name] = newType\n\talias2hash[alias] = newType\n\tSupported = append(Supported, newType)\n\treturn newType\n}\n\nvar (\n\t// MD5 indicates MD5 support\n\tMD5 = RegisterHash(\"md5\", \"MD5\", 32, md5.New)\n\n\t// SHA1 indicates SHA-1 support\n\tSHA1 = RegisterHash(\"sha1\", \"SHA-1\", 40, sha1.New)\n\n\t// SHA256 indicates SHA-256 support\n\tSHA256 = RegisterHash(\"sha256\", \"SHA-256\", 64, sha256.New)\n)\n\n// HashData get hash of one hashType\nfunc HashData(hashType *HashType, data []byte, params ...any) string {\n\th := hashType.NewFunc(params...)\n\th.Write(data)\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// HashReader get hash of one hashType from a reader\nfunc HashReader(hashType *HashType, reader io.Reader, params ...any) (string, error) {\n\th := hashType.NewFunc(params...)\n\t_, err := CopyWithBuffer(h, reader)\n\tif err != nil {\n\t\treturn \"\", errs.NewErr(err, \"HashReader error\")\n\t}\n\treturn hex.EncodeToString(h.Sum(nil)), nil\n}\n\n// HashFile get hash of one hashType from a model.File\nfunc HashFile(hashType *HashType, file io.ReadSeeker, params ...any) (string, error) {\n\tstr, err := HashReader(hashType, file, params...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err = file.Seek(0, io.SeekStart); err != nil {\n\t\treturn str, err\n\t}\n\treturn str, nil\n}\n\n// fromTypes will return hashers for all the requested types.\nfunc fromTypes(types []*HashType) map[*HashType]hash.Hash {\n\thashers := map[*HashType]hash.Hash{}\n\tfor _, t := range types {\n\t\thashers[t] = t.NewFunc()\n\t}\n\treturn hashers\n}\n\n// toMultiWriter will return a set of hashers into a\n// single multiwriter, where one write will update all\n// the hashers.\nfunc toMultiWriter(h map[*HashType]hash.Hash) io.Writer {\n\t// Convert to to slice\n\tvar w = make([]io.Writer, 0, len(h))\n\tfor _, v := range h {\n\t\tw = append(w, v)\n\t}\n\treturn io.MultiWriter(w...)\n}\n\n// A MultiHasher will construct various hashes on all incoming writes.\ntype MultiHasher struct {\n\tw    io.Writer\n\tsize int64\n\th    map[*HashType]hash.Hash // Hashes\n}\n\n// NewMultiHasher will return a hash writer that will write\n// the requested hash types.\nfunc NewMultiHasher(types []*HashType) *MultiHasher {\n\thashers := fromTypes(types)\n\tm := MultiHasher{h: hashers, w: toMultiWriter(hashers)}\n\treturn &m\n}\n\nfunc (m *MultiHasher) Write(p []byte) (n int, err error) {\n\tn, err = m.w.Write(p)\n\tm.size += int64(n)\n\treturn n, err\n}\n\nfunc (m *MultiHasher) GetHashInfo() *HashInfo {\n\tdst := make(map[*HashType]string)\n\tfor k, v := range m.h {\n\t\tdst[k] = hex.EncodeToString(v.Sum(nil))\n\t}\n\treturn &HashInfo{h: dst}\n}\n\n// Sum returns the specified hash from the multihasher\nfunc (m *MultiHasher) Sum(hashType *HashType) ([]byte, error) {\n\th, ok := m.h[hashType]\n\tif !ok {\n\t\treturn nil, ErrUnsupported\n\t}\n\treturn h.Sum(nil), nil\n}\n\n// Size returns the number of bytes written\nfunc (m *MultiHasher) Size() int64 {\n\treturn m.size\n}\n\n// A HashInfo contains hash string for one or more hashType\ntype HashInfo struct {\n\th map[*HashType]string `json:\"hashInfo\"`\n}\n\nfunc NewHashInfoByMap(h map[*HashType]string) HashInfo {\n\treturn HashInfo{h}\n}\n\nfunc NewHashInfo(ht *HashType, str string) HashInfo {\n\tm := make(map[*HashType]string)\n\tif ht != nil {\n\t\tm[ht] = str\n\t}\n\treturn HashInfo{h: m}\n}\n\nfunc (hi HashInfo) String() string {\n\tresult, err := json.Marshal(hi.h)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(result)\n}\nfunc FromString(str string) HashInfo {\n\thi := NewHashInfo(nil, \"\")\n\tvar tmp map[string]string\n\terr := json.Unmarshal([]byte(str), &tmp)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to unmarsh HashInfo from string=%s\", str)\n\t} else {\n\t\tfor k, v := range tmp {\n\t\t\tif name2hash[k] != nil && len(v) > 0 {\n\t\t\t\thi.h[name2hash[k]] = v\n\t\t\t}\n\t\t}\n\t}\n\n\treturn hi\n}\nfunc (hi HashInfo) GetHash(ht *HashType) string {\n\treturn hi.h[ht]\n}\n\nfunc (hi HashInfo) Export() map[*HashType]string {\n\treturn hi.h\n}\n\nfunc (hi HashInfo) All() iter.Seq2[*HashType, string] {\n\treturn func(yield func(*HashType, string) bool) {\n\t\tfor hashType, hashValue := range hi.h {\n\t\t\tif !yield(hashType, hashValue) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/hash_test.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\ntype hashTest struct {\n\tinput  []byte\n\toutput map[*HashType]string\n}\n\nvar hashTestSet = []hashTest{\n\t{\n\t\tinput: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14},\n\t\toutput: map[*HashType]string{\n\t\t\tMD5:    \"bf13fc19e5151ac57d4252e0e0f87abe\",\n\t\t\tSHA1:   \"3ab6543c08a75f292a5ecedac87ec41642d12166\",\n\t\t\tSHA256: \"c839e57675862af5c21bd0a15413c3ec579e0d5522dab600bc6c3489b05b8f54\",\n\t\t},\n\t},\n\t// Empty data set\n\t{\n\t\tinput: []byte{},\n\t\toutput: map[*HashType]string{\n\t\t\tMD5:    \"d41d8cd98f00b204e9800998ecf8427e\",\n\t\t\tSHA1:   \"da39a3ee5e6b4b0d3255bfef95601890afd80709\",\n\t\t\tSHA256: \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\n\t\t},\n\t},\n}\n\nfunc TestMultiHasher(t *testing.T) {\n\tfor _, test := range hashTestSet {\n\t\tmh := NewMultiHasher([]*HashType{MD5, SHA1, SHA256})\n\t\tn, err := CopyWithBuffer(mh, bytes.NewBuffer(test.input))\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, test.input, int(n))\n\t\thashInfo := mh.GetHashInfo()\n\t\tfor k, v := range hashInfo.h {\n\t\t\texpect, ok := test.output[k]\n\t\t\trequire.True(t, ok, \"test output for hash not found\")\n\t\t\tassert.Equal(t, expect, v)\n\t\t}\n\t\t// Test that all are present\n\t\tfor k, v := range test.output {\n\t\t\texpect, ok := hashInfo.h[k]\n\t\t\trequire.True(t, ok, \"test output for hash not found\")\n\t\t\tassert.Equal(t, expect, v)\n\t\t}\n\t\tfor k, v := range test.output {\n\t\t\texpect := hashInfo.GetHash(k)\n\t\t\trequire.True(t, len(expect) > 0, \"test output for hash not found\")\n\t\t\tassert.Equal(t, expect, v)\n\t\t}\n\t\texpect := hashInfo.GetHash(nil)\n\t\trequire.True(t, len(expect) == 0, \"unknown type should return empty string\")\n\t\tstr := hashInfo.String()\n\t\tLog.Info(\"str=\" + str)\n\t\tnewHi := FromString(str)\n\t\tassert.Equal(t, newHi.h, hashInfo.h)\n\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/html.go",
    "content": "package utils\n\nimport \"github.com/microcosm-cc/bluemonday\"\n\nvar htmlSanitizePolicy = bluemonday.StrictPolicy()\n\nfunc SanitizeHTML(s string) string {\n\treturn htmlSanitizePolicy.Sanitize(s)\n}\n"
  },
  {
    "path": "pkg/utils/http.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// GenerateContentDisposition 生成符合RFC 5987标准的Content-Disposition头部\nfunc GenerateContentDisposition(fileName string) string {\n\t// 按照RFC 2047进行编码，用于filename部分\n\tencodedName := urlEncode(fileName)\n\n\t// 按照RFC 5987进行编码，用于filename*部分\n\tencodedNameRFC5987 := encodeRFC5987(fileName)\n\n\treturn fmt.Sprintf(\"attachment; filename=\\\"%s\\\"; filename*=utf-8''%s\",\n\t\tencodedName, encodedNameRFC5987)\n}\n\n// encodeRFC5987 按照RFC 5987规范编码字符串，适用于HTTP头部参数中的非ASCII字符\nfunc encodeRFC5987(s string) string {\n\tvar buf strings.Builder\n\tfor _, r := range []byte(s) {\n\t\t// 根据RFC 5987，只有字母、数字和部分特殊符号可以不编码\n\t\tif (r >= 'a' && r <= 'z') ||\n\t\t\t(r >= 'A' && r <= 'Z') ||\n\t\t\t(r >= '0' && r <= '9') ||\n\t\t\tr == '-' || r == '.' || r == '_' || r == '~' {\n\t\t\tbuf.WriteByte(r)\n\t\t} else {\n\t\t\t// 其他字符都需要百分号编码\n\t\t\tfmt.Fprintf(&buf, \"%%%02X\", r)\n\t\t}\n\t}\n\treturn buf.String()\n}\n\nfunc urlEncode(s string) string {\n\ts = url.QueryEscape(s)\n\ts = strings.ReplaceAll(s, \"+\", \"%20\")\n\treturn s\n}\n"
  },
  {
    "path": "pkg/utils/io.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// here is some syntaxic sugar inspired by the Tomas Senart's video,\n// it allows me to inline the Reader interface\ntype readerFunc func(p []byte) (n int, err error)\n\nfunc (rf readerFunc) Read(p []byte) (n int, err error) { return rf(p) }\n\n// CopyWithCtx slightly modified function signature:\n// - context has been added in order to propagate cancellation\n// - I do not return the number of bytes written, has it is not useful in my use case\nfunc CopyWithCtx(ctx context.Context, out io.Writer, in io.Reader, size int64, progress func(percentage float64)) error {\n\t// Copy will call the Reader and Writer interface multiple time, in order\n\t// to copy by chunk (avoiding loading the whole file in memory).\n\t// I insert the ability to cancel before read time as it is the earliest\n\t// possible in the call process.\n\tvar finish int64 = 0\n\ts := size / 100\n\t_, err := CopyWithBuffer(out, readerFunc(func(p []byte) (int, error) {\n\t\t// golang non-blocking channel: https://gobyexample.com/non-blocking-channel-operations\n\t\tselect {\n\t\t// if context has been canceled\n\t\tcase <-ctx.Done():\n\t\t\t// stop process and propagate \"context canceled\" error\n\t\t\treturn 0, ctx.Err()\n\t\tdefault:\n\t\t\t// otherwise just run default io.Reader implementation\n\t\t\tn, err := in.Read(p)\n\t\t\tif s > 0 && (err == nil || err == io.EOF) {\n\t\t\t\tfinish += int64(n)\n\t\t\t\tprogress(float64(finish) / float64(s))\n\t\t\t}\n\t\t\treturn n, err\n\t\t}\n\t}))\n\treturn err\n}\n\ntype limitWriter struct {\n\tw     io.Writer\n\tlimit int64\n}\n\nfunc (l *limitWriter) Write(p []byte) (n int, err error) {\n\tlp := len(p)\n\tif l.limit > 0 {\n\t\tif int64(lp) > l.limit {\n\t\t\tp = p[:l.limit]\n\t\t}\n\t\tl.limit -= int64(len(p))\n\t\t_, err = l.w.Write(p)\n\t}\n\treturn lp, err\n}\n\nfunc LimitWriter(w io.Writer, limit int64) io.Writer {\n\treturn &limitWriter{w: w, limit: limit}\n}\n\ntype ReadCloser struct {\n\tio.Reader\n\tio.Closer\n}\n\ntype CloseFunc func() error\n\nfunc (c CloseFunc) Close() error {\n\treturn c()\n}\n\nfunc NewReadCloser(reader io.Reader, close CloseFunc) io.ReadCloser {\n\treturn ReadCloser{\n\t\tReader: reader,\n\t\tCloser: close,\n\t}\n}\n\nfunc NewLimitReadCloser(reader io.Reader, close CloseFunc, limit int64) io.ReadCloser {\n\treturn NewReadCloser(io.LimitReader(reader, limit), close)\n}\n\ntype MultiReadable struct {\n\toriginReader io.Reader\n\treader       io.Reader\n\tcache        *bytes.Buffer\n}\n\nfunc NewMultiReadable(reader io.Reader) *MultiReadable {\n\treturn &MultiReadable{\n\t\toriginReader: reader,\n\t\treader:       reader,\n\t}\n}\n\nfunc (mr *MultiReadable) Read(p []byte) (int, error) {\n\tn, err := mr.reader.Read(p)\n\tif _, ok := mr.reader.(io.Seeker); !ok && n > 0 {\n\t\tif mr.cache == nil {\n\t\t\tmr.cache = &bytes.Buffer{}\n\t\t}\n\t\tmr.cache.Write(p[:n])\n\t}\n\treturn n, err\n}\n\nfunc (mr *MultiReadable) Reset() error {\n\tif seeker, ok := mr.reader.(io.Seeker); ok {\n\t\t_, err := seeker.Seek(0, io.SeekStart)\n\t\treturn err\n\t}\n\tif mr.cache != nil && mr.cache.Len() > 0 {\n\t\tmr.reader = io.MultiReader(mr.cache, mr.reader)\n\t\tmr.cache = nil\n\t}\n\treturn nil\n}\n\nfunc (mr *MultiReadable) Close() error {\n\tif closer, ok := mr.originReader.(io.Closer); ok {\n\t\treturn closer.Close()\n\t}\n\treturn nil\n}\n\nfunc Retry(attempts int, sleep time.Duration, f func() error) (err error) {\n\tfor i := 0; i < attempts; i++ {\n\t\t//fmt.Println(\"This is attempt number\", i)\n\t\tif i > 0 {\n\t\t\tlog.Println(\"retrying after error:\", err)\n\t\t\ttime.Sleep(sleep)\n\t\t\tsleep *= 2\n\t\t}\n\t\terr = f()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"after %d attempts, last error: %s\", attempts, err)\n}\n\ntype ClosersIF interface {\n\tio.Closer\n\tAdd(closer io.Closer)\n\tAddIfCloser(a any)\n}\ntype Closers []io.Closer\n\nfunc (c *Closers) Close() error {\n\tvar errs []error\n\tfor _, closer := range *c {\n\t\tif closer != nil {\n\t\t\terrs = append(errs, closer.Close())\n\t\t}\n\t}\n\tclear(*c)\n\t*c = (*c)[:0]\n\treturn errors.Join(errs...)\n}\nfunc (c *Closers) Add(closer io.Closer) {\n\tif closer != nil {\n\t\t*c = append(*c, closer)\n\t}\n}\nfunc (c *Closers) AddIfCloser(a any) {\n\tif closer, ok := a.(io.Closer); ok {\n\t\t*c = append(*c, closer)\n\t}\n}\n\nvar _ ClosersIF = (*Closers)(nil)\n\nfunc NewClosers(c ...io.Closer) Closers {\n\treturn Closers(c)\n}\n\ntype SyncClosers struct {\n\tclosers []io.Closer\n\tref     int32\n}\n\n// if closed, return false\nfunc (c *SyncClosers) AcquireReference() bool {\n\tref := atomic.AddInt32(&c.ref, 1)\n\tif ref > 0 {\n\t\t// log.Debugf(\"AcquireReference %p: %d\", c, ref)\n\t\treturn true\n\t}\n\tatomic.StoreInt32(&c.ref, closersClosed)\n\treturn false\n}\n\nconst closersClosed = math.MinInt32\n\nfunc (c *SyncClosers) Close() error {\n\tfor {\n\t\tref := atomic.LoadInt32(&c.ref)\n\t\tif ref < 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif ref > 1 {\n\t\t\tif atomic.CompareAndSwapInt32(&c.ref, ref, ref-1) {\n\t\t\t\t// log.Debugf(\"ReleaseReference %p: %d\", c, ref)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t} else if atomic.CompareAndSwapInt32(&c.ref, ref, closersClosed) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// log.Debugf(\"FinalClose %p\", c)\n\tvar errs []error\n\tfor _, closer := range c.closers {\n\t\tif closer != nil {\n\t\t\terrs = append(errs, closer.Close())\n\t\t}\n\t}\n\tclear(c.closers)\n\tc.closers = nil\n\treturn errors.Join(errs...)\n}\n\nfunc (c *SyncClosers) Add(closer io.Closer) {\n\tif closer != nil {\n\t\tif atomic.LoadInt32(&c.ref) < 0 {\n\t\t\tpanic(\"Not reusable\")\n\t\t}\n\t\tc.closers = append(c.closers, closer)\n\t}\n}\n\nfunc (c *SyncClosers) AddIfCloser(a any) {\n\tif closer, ok := a.(io.Closer); ok {\n\t\tif atomic.LoadInt32(&c.ref) < 0 {\n\t\t\tpanic(\"Not reusable\")\n\t\t}\n\t\tc.closers = append(c.closers, closer)\n\t}\n}\n\nvar _ ClosersIF = (*SyncClosers)(nil)\n\n// 实现cache.Expirable接口\nfunc (c *SyncClosers) Expired() bool {\n\treturn atomic.LoadInt32(&c.ref) < 0\n}\nfunc (c *SyncClosers) Length() int {\n\treturn len(c.closers)\n}\n\nfunc NewSyncClosers(c ...io.Closer) SyncClosers {\n\treturn SyncClosers{closers: c}\n}\n\ntype Ordered interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 |\n\t\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |\n\t\t~float32 | ~float64 |\n\t\t~string\n}\n\nfunc Min[T Ordered](a, b T) T {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc Max[T Ordered](a, b T) T {\n\tif a < b {\n\t\treturn b\n\t}\n\treturn a\n}\n\nvar IoBuffPool = &sync.Pool{\n\tNew: func() interface{} {\n\t\treturn make([]byte, 32*1024*2) // Two times of size in io package\n\t},\n}\n\nfunc CopyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) {\n\tbuff := IoBuffPool.Get().([]byte)\n\tdefer IoBuffPool.Put(buff)\n\treturn io.CopyBuffer(dst, src, buff)\n}\n\nfunc CopyWithBufferN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {\n\twritten, err = CopyWithBuffer(dst, io.LimitReader(src, n))\n\tif written == n {\n\t\treturn n, nil\n\t}\n\tif written < n && err == nil {\n\t\t// src stopped early; must have been EOF.\n\t\terr = io.EOF\n\t}\n\treturn\n}\n"
  },
  {
    "path": "pkg/utils/ip.go",
    "content": "package utils\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc ClientIP(r *http.Request) string {\n\txForwardedFor := r.Header.Get(\"X-Forwarded-For\")\n\tip := strings.TrimSpace(strings.Split(xForwardedFor, \",\")[0])\n\tif ip != \"\" {\n\t\treturn ip\n\t}\n\n\tip = strings.TrimSpace(r.Header.Get(\"X-Real-Ip\"))\n\tif ip != \"\" {\n\t\treturn ip\n\t}\n\n\tif ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {\n\t\treturn ip\n\t}\n\n\treturn \"\"\n}\n\nfunc IsLocalIPAddr(ip string) bool {\n\treturn IsLocalIP(net.ParseIP(ip))\n}\n\nfunc IsLocalIP(ip net.IP) bool {\n\tif ip == nil {\n\t\treturn false\n\t}\n\tif ip.IsLoopback() {\n\t\treturn true\n\t}\n\n\tip4 := ip.To4()\n\tif ip4 == nil {\n\t\treturn false\n\t}\n\n\treturn ip4[0] == 10 || // 10.0.0.0/8\n\t\t(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12\n\t\t(ip4[0] == 169 && ip4[1] == 254) || // 169.254.0.0/16\n\t\t(ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16\n}\n"
  },
  {
    "path": "pkg/utils/json.go",
    "content": "package utils\n\nimport (\n\tstdjson \"encoding/json\"\n\t\"os\"\n\n\tjson \"github.com/json-iterator/go\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar Json = json.ConfigCompatibleWithStandardLibrary\n\n// WriteJsonToFile write struct to json file\nfunc WriteJsonToFile(dst string, data interface{}, std ...bool) bool {\n\tstr, err := json.MarshalIndent(data, \"\", \"  \")\n\tif len(std) > 0 && std[0] {\n\t\tstr, err = stdjson.MarshalIndent(data, \"\", \"  \")\n\t}\n\tif err != nil {\n\t\tlog.Errorf(\"failed convert Conf to []byte:%s\", err.Error())\n\t\treturn false\n\t}\n\terr = os.WriteFile(dst, str, 0777)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to write json file:%s\", err.Error())\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "pkg/utils/log.go",
    "content": "package utils\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar Log = log.New()\n"
  },
  {
    "path": "pkg/utils/map.go",
    "content": "package utils\n\nfunc MergeMap(mObj ...map[string]interface{}) map[string]interface{} {\n\tnewObj := map[string]interface{}{}\n\tfor _, m := range mObj {\n\t\tfor k, v := range m {\n\t\t\tnewObj[k] = v\n\t\t}\n\t}\n\treturn newObj\n}\n"
  },
  {
    "path": "pkg/utils/oauth2.go",
    "content": "package utils\n\nimport \"golang.org/x/oauth2\"\n\ntype tokenSource struct {\n\tfn func() (*oauth2.Token, error)\n}\n\nfunc (t *tokenSource) Token() (*oauth2.Token, error) {\n\treturn t.fn()\n}\n\nfunc TokenSource(fn func() (*oauth2.Token, error)) oauth2.TokenSource {\n\treturn &tokenSource{fn}\n}\n"
  },
  {
    "path": "pkg/utils/path.go",
    "content": "package utils\n\nimport (\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n)\n\n// FixAndCleanPath\n// The upper layer of the root directory is still the root directory.\n// So \"..\" And \".\" will be cleared\n// for example\n// 1. \"..\" or \".\" => \"/\"\n// 2. \"../...\" or \"./...\" => \"/...\"\n// 3. \"../.x.\" or \"./.x.\" => \"/.x.\"\n// 4. \"x//\\\\y\" = > \"/z/x\"\nfunc FixAndCleanPath(path string) string {\n\tpath = strings.ReplaceAll(path, \"\\\\\", \"/\")\n\tif !strings.HasPrefix(path, \"/\") {\n\t\tpath = \"/\" + path\n\t}\n\treturn stdpath.Clean(path)\n}\n\n// PathAddSeparatorSuffix Add path '/' suffix\n// for example /root => /root/\nfunc PathAddSeparatorSuffix(path string) string {\n\tif !strings.HasSuffix(path, \"/\") {\n\t\tpath = path + \"/\"\n\t}\n\treturn path\n}\n\n// PathEqual judge path is equal\nfunc PathEqual(path1, path2 string) bool {\n\treturn FixAndCleanPath(path1) == FixAndCleanPath(path2)\n}\n\nfunc IsSubPath(path string, subPath string) bool {\n\tpath, subPath = FixAndCleanPath(path), FixAndCleanPath(subPath)\n\treturn path == subPath || strings.HasPrefix(subPath, PathAddSeparatorSuffix(path))\n}\n\nfunc Ext(path string) string {\n\treturn strings.ToLower(SourceExt(path))\n}\n\nfunc SourceExt(path string) string {\n\text := stdpath.Ext(path)\n\tif len(ext) > 0 && ext[0] == '.' {\n\t\text = ext[1:]\n\t}\n\treturn ext\n}\n\nfunc EncodePath(path string, all ...bool) string {\n\tseg := strings.Split(path, \"/\")\n\ttoReplace := []struct {\n\t\tSrc string\n\t\tDst string\n\t}{\n\t\t{Src: \"%\", Dst: \"%25\"},\n\t\t{\"%\", \"%25\"},\n\t\t{\"?\", \"%3F\"},\n\t\t{\"#\", \"%23\"},\n\t}\n\tfor i := range seg {\n\t\tif len(all) > 0 && all[0] {\n\t\t\tseg[i] = url.PathEscape(seg[i])\n\t\t} else {\n\t\t\tfor j := range toReplace {\n\t\t\t\tseg[i] = strings.ReplaceAll(seg[i], toReplace[j].Src, toReplace[j].Dst)\n\t\t\t}\n\t\t}\n\t}\n\treturn strings.Join(seg, \"/\")\n}\n\nfunc JoinBasePath(basePath, reqPath string) (string, error) {\n\tisRelativePath := strings.Contains(reqPath, \"..\")\n\treqPath = FixAndCleanPath(reqPath)\n\tif isRelativePath && !strings.Contains(reqPath, \"..\") {\n\t\treturn \"\", errs.RelativePath\n\t}\n\treturn stdpath.Join(FixAndCleanPath(basePath), reqPath), nil\n}\n\nfunc GetFullPath(mountPath, path string) string {\n\treturn stdpath.Join(GetActualMountPath(mountPath), path)\n}\n\n// GetPathHierarchy generates a hierarchy of paths from the given path.\n//\n// Example:\n//  1. \"/\" => {\"/\"}\n//  2. \"\" => {\"/\"}\n//  3. \"/a/b/c\" => {\"/\", \"/a\", \"/a/b\", \"/a/b/c\"}\n//  4. \"/a/b/c/d/e.txt\" => {\"/\", \"/a\", \"/a/b\", \"/a/b/c\", \"/a/b/c/d\", \"/a/b/c/d/e.txt\"}\n//  5. \"./a/b///c\" => {\"/\", \"/a\", \"/a/b\", \"/a/b/c\"}\nfunc GetPathHierarchy(path string) []string {\n\tif path == \"\" || path == \"/\" {\n\t\treturn []string{\"/\"}\n\t}\n\n\tpath = FixAndCleanPath(path)\n\tif !strings.HasPrefix(path, \"/\") {\n\t\tpath = \"/\" + path\n\t}\n\n\thierarchy := []string{\"/\"}\n\n\tparts := strings.Split(path, \"/\")\n\tcurrentPath := \"\"\n\tfor _, part := range parts {\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tcurrentPath += \"/\" + part\n\t\thierarchy = append(hierarchy, currentPath)\n\t}\n\n\treturn hierarchy\n}\n"
  },
  {
    "path": "pkg/utils/path_test.go",
    "content": "package utils\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestEncodePath(t *testing.T) {\n\tt.Log(EncodePath(\"http://localhost:5244/d/123#.png\"))\n}\n\nfunc TestFixAndCleanPath(t *testing.T) {\n\tdatas := map[string]string{\n\t\t\"\":                          \"/\",\n\t\t\".././\":                     \"/\",\n\t\t\"../../.../\":                \"/...\",\n\t\t\"x//\\\\y/\":                   \"/x/y\",\n\t\t\".././.x/.y/.//..x../..y..\": \"/.x/.y/..x../..y..\",\n\t}\n\tfor key, value := range datas {\n\t\tif FixAndCleanPath(key) != value {\n\t\t\tt.Logf(\"raw %s fix fail\", key)\n\t\t}\n\t}\n}\n\nfunc TestGetPathHierarchy(t *testing.T) {\n\ttestCases := map[string][]string{\n\t\t\"\":                                    {\"/\"},\n\t\t\"/\":                                   {\"/\"},\n\t\t\"/home\":                               {\"/\", \"/home\"},\n\t\t\"/home/user\":                          {\"/\", \"/home\", \"/home/user\"},\n\t\t\"/home/user/documents\":                {\"/\", \"/home\", \"/home/user\", \"/home/user/documents\"},\n\t\t\"/home/user/documents/files/test.txt\": {\"/\", \"/home\", \"/home/user\", \"/home/user/documents\", \"/home/user/documents/files\", \"/home/user/documents/files/test.txt\"},\n\t\t\"home\":                                {\"/\", \"/home\"},\n\t\t\"home/user\":                           {\"/\", \"/home\", \"/home/user\"},\n\t\t\"./home/\":                             {\"/\", \"/home\"},\n\t\t\"..//home//user/../././\":              {\"/\", \"/home\"},\n\t\t\"/home///user///documents///\":         {\"/\", \"/home\", \"/home/user\", \"/home/user/documents\"},\n\t\t\"/home/user with spaces/doc\":          {\"/\", \"/home\", \"/home/user with spaces\", \"/home/user with spaces/doc\"},\n\t\t\"/home/user@domain.com/files\":         {\"/\", \"/home\", \"/home/user@domain.com\", \"/home/user@domain.com/files\"},\n\t\t\"/home/.hidden/.config\":               {\"/\", \"/home\", \"/home/.hidden\", \"/home/.hidden/.config\"},\n\t}\n\n\tfor input, expected := range testCases {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\tresult := GetPathHierarchy(input)\n\t\t\tif !reflect.DeepEqual(result, expected) {\n\t\t\t\tt.Errorf(\"GetPathHierarchy(%q) = %v, want %v\", input, result, expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/random/random.go",
    "content": "package random\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n\tmathRand \"math/rand\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\nvar Rand *mathRand.Rand\n\nconst letterBytes = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n\nfunc String(n int) string {\n\tb := make([]byte, n)\n\tletterLen := big.NewInt(int64(len(letterBytes)))\n\tfor i := range b {\n\t\tidx, err := rand.Int(rand.Reader, letterLen)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tb[i] = letterBytes[idx.Int64()]\n\t}\n\treturn string(b)\n}\n\nfunc Token() string {\n\treturn \"openlist-\" + uuid.NewString() + String(64)\n}\n\nfunc RangeInt64(left, right int64) int64 {\n\treturn mathRand.Int63n(left+right) - left\n}\n\nfunc init() {\n\ts := mathRand.NewSource(time.Now().UnixNano())\n\tRand = mathRand.New(s)\n}\n"
  },
  {
    "path": "pkg/utils/slice.go",
    "content": "package utils\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// SliceEqual check if two slices are equal\nfunc SliceEqual[T comparable](a, b []T) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// SliceContains check if slice contains element\nfunc SliceContains[T comparable](arr []T, v T) bool {\n\tfor _, vv := range arr {\n\t\tif vv == v {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// SliceAllContains check if slice all contains elements\nfunc SliceAllContains[T comparable](arr []T, vs ...T) bool {\n\tvsMap := make(map[T]struct{})\n\tfor _, v := range arr {\n\t\tvsMap[v] = struct{}{}\n\t}\n\tfor _, v := range vs {\n\t\tif _, ok := vsMap[v]; !ok {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// SliceConvert convert slice to another type slice\nfunc SliceConvert[S any, D any](srcS []S, convert func(src S) (D, error)) ([]D, error) {\n\tres := make([]D, 0, len(srcS))\n\tfor i := range srcS {\n\t\tdst, err := convert(srcS[i])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, dst)\n\t}\n\treturn res, nil\n}\n\nfunc MustSliceConvert[S any, D any](srcS []S, convert func(src S) D) []D {\n\tres := make([]D, 0, len(srcS))\n\tfor i := range srcS {\n\t\tdst := convert(srcS[i])\n\t\tres = append(res, dst)\n\t}\n\treturn res\n}\n\nfunc MergeErrors(errs ...error) error {\n\terrStr := strings.Join(MustSliceConvert(errs, func(err error) string {\n\t\treturn err.Error()\n\t}), \"\\n\")\n\tif errStr != \"\" {\n\t\treturn errors.New(errStr)\n\t}\n\treturn nil\n}\n\nfunc SliceMeet[T1, T2 any](arr []T1, v T2, meet func(item T1, v T2) bool) bool {\n\tfor _, item := range arr {\n\t\tif meet(item, v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc SliceFilter[T any](arr []T, filter func(src T) bool) []T {\n\tres := make([]T, 0, len(arr))\n\tfor _, src := range arr {\n\t\tif filter(src) {\n\t\t\tres = append(res, src)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc SliceReplace[T any](arr []T, replace func(src T) T) {\n\tfor i, src := range arr {\n\t\tarr[i] = replace(src)\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/str.go",
    "content": "package utils\n\nimport (\n\t\"encoding/base64\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n)\n\nfunc MappingName(name string) string {\n\tfor k, v := range conf.FilenameCharMap {\n\t\tname = strings.ReplaceAll(name, k, v)\n\t}\n\treturn name\n}\n\nvar DEC = map[string]string{\n\t\"-\": \"+\",\n\t\"_\": \"/\",\n\t\".\": \"=\",\n}\n\nfunc SafeAtob(data string) (string, error) {\n\tfor k, v := range DEC {\n\t\tdata = strings.ReplaceAll(data, k, v)\n\t}\n\tbytes, err := base64.StdEncoding.DecodeString(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(bytes), err\n}\n\n// GetNoneEmpty returns the first non-empty string, return empty if all empty\nfunc GetNoneEmpty(strArr ...string) string {\n\tfor _, s := range strArr {\n\t\tif len(s) > 0 {\n\t\t\treturn s\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/utils/time.go",
    "content": "package utils\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\nvar CNLoc = time.FixedZone(\"UTC\", 8*60*60)\n\nfunc MustParseCNTime(str string) time.Time {\n\tlastOpTime, _ := time.ParseInLocation(\"2006-01-02 15:04:05 -07\", str+\" +08\", CNLoc)\n\treturn lastOpTime\n}\n\nfunc NewDebounce(interval time.Duration) func(f func()) {\n\tvar timer *time.Timer\n\tvar lock sync.Mutex\n\treturn func(f func()) {\n\t\tlock.Lock()\n\t\tdefer lock.Unlock()\n\t\tif timer != nil {\n\t\t\ttimer.Stop()\n\t\t}\n\t\ttimer = time.AfterFunc(interval, f)\n\t}\n}\n\nfunc NewDebounce2(interval time.Duration, f func()) func() {\n\tvar timer *time.Timer\n\tvar lock sync.Mutex\n\treturn func() {\n\t\tlock.Lock()\n\t\tdefer lock.Unlock()\n\t\tif timer == nil {\n\t\t\ttimer = time.AfterFunc(interval, f)\n\t\t}\n\t\ttimer.Reset(interval)\n\t}\n}\n\nfunc NewThrottle(interval time.Duration) func(func()) {\n\tvar lastCall time.Time\n\tvar lock sync.Mutex\n\treturn func(fn func()) {\n\t\tlock.Lock()\n\t\tdefer lock.Unlock()\n\n\t\tnow := time.Now()\n\t\tif now.Sub(lastCall) >= interval {\n\t\t\tlastCall = now\n\t\t\tgo fn()\n\t\t}\n\t}\n}\n\nfunc NewThrottle2(interval time.Duration, fn func()) func() {\n\tvar lastCall time.Time\n\tvar lock sync.Mutex\n\treturn func() {\n\t\tlock.Lock()\n\t\tdefer lock.Unlock()\n\n\t\tnow := time.Now()\n\t\tif now.Sub(lastCall) >= interval {\n\t\t\tlastCall = now\n\t\t\tgo fn()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/url.go",
    "content": "package utils\n\nimport (\n\t\"net/url\"\n)\n\nfunc InjectQuery(raw string, query url.Values) (string, error) {\n\tparam := query.Encode()\n\tif param == \"\" {\n\t\treturn raw, nil\n\t}\n\tu, err := url.Parse(raw)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tjoiner := \"?\"\n\tif u.RawQuery != \"\" {\n\t\tjoiner = \"&\"\n\t}\n\treturn raw + joiner + param, nil\n}\n"
  },
  {
    "path": "public/public.go",
    "content": "package public\n\nimport \"embed\"\n\n//go:embed all:dist\nvar Public embed.FS\n"
  },
  {
    "path": "server/common/auth.go",
    "content": "package common\n\nimport (\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"github.com/golang-jwt/jwt/v4\"\n\t\"github.com/pkg/errors\"\n)\n\nvar SecretKey []byte\n\ntype UserClaims struct {\n\tUsername string `json:\"username\"`\n\tPwdTS    int64  `json:\"pwd_ts\"`\n\tjwt.RegisteredClaims\n}\n\nvar validTokenCache = cache.NewMemCache[bool]()\n\nfunc GenerateToken(user *model.User) (tokenString string, err error) {\n\tclaim := UserClaims{\n\t\tUsername: user.Username,\n\t\tPwdTS:    user.PwdTS,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(conf.Conf.TokenExpiresIn) * time.Hour)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t}}\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)\n\ttokenString, err = token.SignedString(SecretKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvalidTokenCache.Set(tokenString, true)\n\treturn tokenString, err\n}\n\nfunc ParseToken(tokenString string) (*UserClaims, error) {\n\ttoken, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {\n\t\treturn SecretKey, nil\n\t})\n\tif IsTokenInvalidated(tokenString) {\n\t\treturn nil, errors.New(\"token is invalidated\")\n\t}\n\tif err != nil {\n\t\tif ve, ok := err.(*jwt.ValidationError); ok {\n\t\t\tif ve.Errors&jwt.ValidationErrorMalformed != 0 {\n\t\t\t\treturn nil, errors.New(\"that's not even a token\")\n\t\t\t} else if ve.Errors&jwt.ValidationErrorExpired != 0 {\n\t\t\t\treturn nil, errors.New(\"token is expired\")\n\t\t\t} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {\n\t\t\t\treturn nil, errors.New(\"token not active yet\")\n\t\t\t} else {\n\t\t\t\treturn nil, errors.New(\"couldn't handle this token\")\n\t\t\t}\n\t\t}\n\t}\n\tif claims, ok := token.Claims.(*UserClaims); ok && token.Valid {\n\t\treturn claims, nil\n\t}\n\treturn nil, errors.New(\"couldn't handle this token\")\n}\n\nfunc InvalidateToken(tokenString string) error {\n\tif tokenString == \"\" {\n\t\treturn nil // don't invalidate empty guest token\n\t}\n\tvalidTokenCache.Del(tokenString)\n\treturn nil\n}\n\nfunc IsTokenInvalidated(tokenString string) bool {\n\t_, ok := validTokenCache.Get(tokenString)\n\treturn !ok\n}\n"
  },
  {
    "path": "server/common/base.go",
    "content": "package common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n)\n\nfunc GetApiUrlFromRequest(r *http.Request) string {\n\tapi := conf.Conf.SiteURL\n\tif strings.HasPrefix(api, \"http\") {\n\t\treturn strings.TrimSuffix(api, \"/\")\n\t}\n\tif r != nil {\n\t\tprotocol := \"http\"\n\t\tif r.TLS != nil || r.Header.Get(\"X-Forwarded-Proto\") == \"https\" {\n\t\t\tprotocol = \"https\"\n\t\t}\n\t\thost := r.Header.Get(\"X-Forwarded-Host\")\n\t\tif host == \"\" {\n\t\t\thost = r.Host\n\t\t}\n\t\tapi = fmt.Sprintf(\"%s://%s\", protocol, stdpath.Join(host, api))\n\t}\n\tapi = strings.TrimSuffix(api, \"/\")\n\treturn api\n}\n\nfunc GetApiUrl(ctx context.Context) string {\n\tapi, _ := ctx.Value(conf.ApiUrlKey).(string)\n\treturn api\n}\n"
  },
  {
    "path": "server/common/check.go",
    "content": "package common\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/dlclark/regexp2\"\n)\n\nfunc IsStorageSignEnabled(rawPath string) bool {\n\tstorage := op.GetBalancedStorage(rawPath)\n\treturn storage != nil && storage.GetStorage().EnableSign\n}\n\nfunc CanWrite(meta *model.Meta, path string) bool {\n\tif meta == nil || !meta.Write {\n\t\treturn false\n\t}\n\treturn meta.WSub || meta.Path == path\n}\n\nfunc IsApply(metaPath, reqPath string, applySub bool) bool {\n\tif utils.PathEqual(metaPath, reqPath) {\n\t\treturn true\n\t}\n\treturn utils.IsSubPath(metaPath, reqPath) && applySub\n}\n\nfunc CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {\n\t// if the reqPath is in hide (only can check the nearest meta) and user can't see hides, can't access\n\tif meta != nil && !user.CanSeeHides() && meta.Hide != \"\" &&\n\t\tIsApply(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path\n\t\tfor _, hide := range strings.Split(meta.Hide, \"\\n\") {\n\t\t\tre := regexp2.MustCompile(hide, regexp2.None)\n\t\t\tif isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\t// if is not guest and can access without password\n\tif user.CanAccessWithoutPassword() {\n\t\treturn true\n\t}\n\t// if meta is nil or password is empty, can access\n\tif meta == nil || meta.Password == \"\" {\n\t\treturn true\n\t}\n\t// if meta doesn't apply to sub_folder, can access\n\tif !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {\n\t\treturn true\n\t}\n\t// validate password\n\treturn meta.Password == password\n}\n\n// ShouldProxy TODO need optimize\n// when should be proxy?\n// 1. config.MustProxy()\n// 2. storage.WebProxy\n// 3. proxy_types\nfunc ShouldProxy(storage driver.Driver, filename string) bool {\n\tif storage.Config().MustProxy() || storage.GetStorage().WebProxy {\n\t\treturn true\n\t}\n\tif utils.SliceContains(conf.SlicesMap[conf.ProxyTypes], utils.Ext(filename)) {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/common/check_test.go",
    "content": "package common\n\nimport \"testing\"\n\nfunc TestIsApply(t *testing.T) {\n\tdatas := []struct {\n\t\tmetaPath string\n\t\treqPath  string\n\t\tapplySub bool\n\t\tresult   bool\n\t}{\n\t\t{\n\t\t\tmetaPath: \"/\",\n\t\t\treqPath:  \"/test\",\n\t\t\tapplySub: true,\n\t\t\tresult:   true,\n\t\t},\n\t}\n\tfor i, data := range datas {\n\t\tif IsApply(data.metaPath, data.reqPath, data.applySub) != data.result {\n\t\t\tt.Errorf(\"TestIsApply %d failed\", i)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/common/common.go",
    "content": "package common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc hidePrivacy(msg string) string {\n\tfor _, r := range conf.PrivacyReg {\n\t\tmsg = r.ReplaceAllStringFunc(msg, func(s string) string {\n\t\t\treturn strings.Repeat(\"*\", len(s))\n\t\t})\n\t}\n\treturn msg\n}\n\n// ErrorResp is used to return error response\n// @param l: if true, log error\nfunc ErrorResp(c *gin.Context, err error, code int, l ...bool) {\n\tErrorWithDataResp(c, err, code, nil, l...)\n\t//if len(l) > 0 && l[0] {\n\t//\tif flags.Debug || flags.Dev {\n\t//\t\tlog.Errorf(\"%+v\", err)\n\t//\t} else {\n\t//\t\tlog.Errorf(\"%v\", err)\n\t//\t}\n\t//}\n\t//c.JSON(200, Resp[interface{}]{\n\t//\tCode:    code,\n\t//\tMessage: hidePrivacy(err.Error()),\n\t//\tData:    nil,\n\t//})\n\t//c.Abort()\n}\n\n// ErrorPage is used to return error page HTML.\n// It also returns standard HTTP status code.\n// @param l: if true, log error\nfunc ErrorPage(c *gin.Context, err error, code int, l ...bool) {\n\n\tif len(l) > 0 && l[0] {\n\t\tif flags.Debug || flags.Dev {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t} else {\n\t\t\tlog.Errorf(\"%v\", err)\n\t\t}\n\t}\n\n\tcodes := fmt.Sprintf(\"%d %s\", code, http.StatusText(code))\n\n\thtml := fmt.Sprintf(`<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta name=\"color-scheme\" content=\"dark light\" />\n\t\t<meta name=\"robots\" content=\"noindex\" />\n\t\t<title>%s</title>\n\t</head>\n\t<body>\n\t\t<h1>%s</h1>\n\t\t<hr>\n\t\t<p>%s</p>\n\t</body>\n</html>`,\n\t\tcodes, codes, html.EscapeString(hidePrivacy(err.Error())))\n\tc.Data(code, \"text/html; charset=utf-8\", []byte(html))\n\tc.Abort()\n}\n\nfunc ErrorWithDataResp(c *gin.Context, err error, code int, data interface{}, l ...bool) {\n\tif len(l) > 0 && l[0] {\n\t\tif flags.Debug || flags.Dev {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t} else {\n\t\t\tlog.Errorf(\"%v\", err)\n\t\t}\n\t}\n\tc.JSON(200, Resp[interface{}]{\n\t\tCode:    code,\n\t\tMessage: hidePrivacy(err.Error()),\n\t\tData:    data,\n\t})\n\tc.Abort()\n}\n\nfunc ErrorStrResp(c *gin.Context, str string, code int, l ...bool) {\n\tif len(l) != 0 && l[0] {\n\t\tlog.Error(str)\n\t}\n\tc.JSON(200, Resp[interface{}]{\n\t\tCode:    code,\n\t\tMessage: hidePrivacy(str),\n\t\tData:    nil,\n\t})\n\tc.Abort()\n}\n\nfunc SuccessResp(c *gin.Context, data ...interface{}) {\n\tSuccessWithMsgResp(c, \"success\", data...)\n}\n\nfunc SuccessWithMsgResp(c *gin.Context, msg string, data ...interface{}) {\n\tvar respData interface{}\n\tif len(data) > 0 {\n\t\trespData = data[0]\n\t}\n\n\tc.JSON(200, Resp[interface{}]{\n\t\tCode:    200,\n\t\tMessage: msg,\n\t\tData:    respData,\n\t})\n}\n\nfunc Pluralize(count int, singular, plural string) string {\n\tif count == 1 {\n\t\treturn singular\n\t}\n\treturn plural\n}\n\nfunc GinWithValue(c *gin.Context, keyAndValue ...any) {\n\tc.Request = c.Request.WithContext(\n\t\tContentWithValue(c.Request.Context(), keyAndValue...),\n\t)\n}\n\nfunc ContentWithValue(ctx context.Context, keyAndValue ...any) context.Context {\n\tif len(keyAndValue) < 1 || len(keyAndValue)%2 != 0 {\n\t\tpanic(\"keyAndValue must be an even number of arguments (key, value, ...)\")\n\t}\n\tfor len(keyAndValue) > 0 {\n\t\tctx = context.WithValue(ctx, keyAndValue[0], keyAndValue[1])\n\t\tkeyAndValue = keyAndValue[2:]\n\t}\n\treturn ctx\n}\n"
  },
  {
    "path": "server/common/hide_privacy_test.go",
    "content": "package common\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n)\n\nfunc TestHidePrivacy(t *testing.T) {\n\treg, err := regexp.Compile(\"(?U)access_token=(.*)&\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tconf.PrivacyReg = []*regexp.Regexp{reg}\n\tres := hidePrivacy(`Get \"https://pan.baidu.com/rest/2.0/xpan/file?access_token=121.d1f66e95acfa40274920079396a51c48.Y2aP2vQDq90hLBE3PAbVije59uTcn7GiWUfw8LCM_olw&dir=%2F&limit=200&method=list&order=name&start=0&web=web \" : net/http: TLS handshake timeout`)\n\tt.Log(res)\n}\n"
  },
  {
    "path": "server/common/ldap.go",
    "content": "package common\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/ldap.v3\"\n)\n\nvar ErrFailedLdapAuth = errors.New(\"failed to auth\")\n\nfunc HandleLdapLogin(username, password string) error {\n\t// Auth start\n\tldapServer := setting.GetStr(conf.LdapServer)\n\tskipTlsVerify := setting.GetBool(conf.LdapSkipTlsVerify)\n\tldapManagerDN := setting.GetStr(conf.LdapManagerDN)\n\tldapManagerPassword := setting.GetStr(conf.LdapManagerPassword)\n\tldapUserSearchBase := setting.GetStr(conf.LdapUserSearchBase)\n\tldapUserSearchFilter := setting.GetStr(conf.LdapUserSearchFilter) // (uid=%s)\n\n\t// Connect to LdapServer\n\tl, err := dial(ldapServer, skipTlsVerify)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to connect to LDAP\")\n\t}\n\tdefer l.Close()\n\n\t// First bind with a read only user\n\tif ldapManagerDN != \"\" && ldapManagerPassword != \"\" {\n\t\terr = l.Bind(ldapManagerDN, ldapManagerPassword)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessagef(err, \"failed to bind to LDAP\")\n\t\t}\n\t}\n\n\t// Search for the given username\n\tsearchRequest := ldap.NewSearchRequest(\n\t\tldapUserSearchBase,\n\t\tldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,\n\t\tfmt.Sprintf(ldapUserSearchFilter, ldap.EscapeFilter(username)),\n\t\t[]string{\"dn\"},\n\t\tnil,\n\t)\n\tsr, err := l.Search(searchRequest)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed login ldap: LDAP search failed\")\n\t}\n\tif len(sr.Entries) != 1 {\n\t\treturn errors.New(\"failed login ldap: user does not exist or too many entries returned\")\n\t}\n\tuserDN := sr.Entries[0].DN\n\n\t// Bind as the user to verify their password\n\terr = l.Bind(userDN, password)\n\tif err != nil {\n\t\treturn errors.WithMessagef(ErrFailedLdapAuth, \"%v\", err)\n\t}\n\tlog.Infof(\"LDAP auth successful for %s\", username)\n\t// Auth finished\n\treturn nil\n}\n\nfunc LdapRegister(username string) (*model.User, error) {\n\tif username == \"\" {\n\t\treturn nil, errors.New(\"cannot get username from ldap provider\")\n\t}\n\tuser := &model.User{\n\t\tUsername:   username,\n\t\tPassword:   \"\",\n\t\tAuthn:      \"[]\",\n\t\tPermission: int32(setting.GetInt(conf.LdapDefaultPermission, 0)),\n\t\tBasePath:   setting.GetStr(conf.LdapDefaultDir),\n\t\tRole:       0,\n\t\tDisabled:   false,\n\t\tAllowLdap:  true,\n\t}\n\tuser.SetPassword(random.String(16))\n\tif err := op.CreateUser(user); err != nil {\n\t\treturn nil, err\n\t}\n\treturn user, nil\n}\n\nfunc dial(ldapServer string, skipTlsVerify ...bool) (*ldap.Conn, error) {\n\ttlsEnabled := false\n\tif strings.HasPrefix(ldapServer, \"ldaps://\") {\n\t\ttlsEnabled = true\n\t\tldapServer = strings.TrimPrefix(ldapServer, \"ldaps://\")\n\t} else if strings.HasPrefix(ldapServer, \"ldap://\") {\n\t\tldapServer = strings.TrimPrefix(ldapServer, \"ldap://\")\n\t}\n\n\tif tlsEnabled {\n\t\treturn ldap.DialTLS(\"tcp\", ldapServer, &tls.Config{InsecureSkipVerify: utils.IsBool(skipTlsVerify...)})\n\t} else {\n\t\treturn ldap.Dial(\"tcp\", ldapServer)\n\t}\n}\n"
  },
  {
    "path": "server/common/proxy.go",
    "content": "package common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"maps\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\nfunc Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {\n\t// if link.MFile != nil {\n\t// \tattachHeader(w, file, link)\n\t// \thttp.ServeContent(w, r, file.GetName(), file.ModTime(), link.MFile)\n\t// \treturn nil\n\t// }\n\n\tif link.Concurrency > 0 || link.PartSize > 0 {\n\t\tattachHeader(w, file, link)\n\t\tsize := link.ContentLength\n\t\tif size <= 0 {\n\t\t\tsize = file.GetSize()\n\t\t}\n\t\trrf, _ := stream.GetRangeReaderFromLink(size, link)\n\t\tif link.RangeReader == nil {\n\t\t\tr = r.WithContext(context.WithValue(r.Context(), conf.RequestHeaderKey, r.Header))\n\t\t}\n\t\treturn net.ServeHTTP(w, r, file.GetName(), file.ModTime(), size, &model.RangeReadCloser{\n\t\t\tRangeReader: rrf,\n\t\t})\n\t}\n\n\tif link.RangeReader != nil {\n\t\tattachHeader(w, file, link)\n\t\tsize := link.ContentLength\n\t\tif size <= 0 {\n\t\t\tsize = file.GetSize()\n\t\t}\n\t\treturn net.ServeHTTP(w, r, file.GetName(), file.ModTime(), size, &model.RangeReadCloser{\n\t\t\tRangeReader: link.RangeReader,\n\t\t})\n\t}\n\n\t//transparent proxy\n\theader := net.ProcessHeader(r.Header, link.Header)\n\tres, err := net.RequestHttp(r.Context(), r.Method, header, link.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tmaps.Copy(w.Header(), res.Header)\n\tw.WriteHeader(res.StatusCode)\n\tif r.Method == http.MethodHead {\n\t\treturn nil\n\t}\n\t_, err = utils.CopyWithBuffer(w, &stream.RateLimitReader{\n\t\tReader:  res.Body,\n\t\tLimiter: stream.ServerDownloadLimit,\n\t\tCtx:     r.Context(),\n\t})\n\treturn err\n}\nfunc attachHeader(w http.ResponseWriter, file model.Obj, link *model.Link) {\n\tfileName := file.GetName()\n\tw.Header().Set(\"Content-Disposition\", utils.GenerateContentDisposition(fileName))\n\tw.Header().Set(\"Content-Type\", utils.GetMimeType(fileName))\n\tsize := link.ContentLength\n\tif size <= 0 {\n\t\tsize = file.GetSize()\n\t}\n\tw.Header().Set(\"Etag\", GetEtag(file, size))\n\tcontentType := link.Header.Get(\"Content-Type\")\n\tif len(contentType) > 0 {\n\t\tw.Header().Set(\"Content-Type\", contentType)\n\t} else {\n\t\tw.Header().Set(\"Content-Type\", utils.GetMimeType(fileName))\n\t}\n}\nfunc GetEtag(file model.Obj, size int64) string {\n\thash := \"\"\n\tfor _, v := range file.GetHash().Export() {\n\t\tif v > hash {\n\t\t\thash = v\n\t\t}\n\t}\n\tif len(hash) > 0 {\n\t\treturn fmt.Sprintf(`\"%s\"`, hash)\n\t}\n\t// 参考nginx\n\treturn fmt.Sprintf(`\"%x-%x\"`, file.ModTime().Unix(), size)\n}\n\nfunc ProxyRange(ctx context.Context, link *model.Link, size int64) *model.Link {\n\tif link.RangeReader == nil && !strings.HasPrefix(link.URL, GetApiUrl(ctx)+\"/\") {\n\t\tif link.ContentLength > 0 {\n\t\t\tsize = link.ContentLength\n\t\t}\n\t\trrf, err := stream.GetRangeReaderFromLink(size, link)\n\t\tif err == nil {\n\t\t\treturn &model.Link{\n\t\t\t\tRangeReader:   rrf,\n\t\t\t\tContentLength: size,\n\t\t\t}\n\t\t}\n\t}\n\treturn link\n}\n\ntype InterceptResponseWriter struct {\n\thttp.ResponseWriter\n\tio.Writer\n}\n\nfunc (iw *InterceptResponseWriter) Write(p []byte) (int, error) {\n\treturn iw.Writer.Write(p)\n}\n\ntype WrittenResponseWriter struct {\n\thttp.ResponseWriter\n\twritten bool\n}\n\nfunc (ww *WrittenResponseWriter) Write(p []byte) (int, error) {\n\tn, err := ww.ResponseWriter.Write(p)\n\tif !ww.written && n > 0 {\n\t\tww.written = true\n\t}\n\treturn n, err\n}\n\nfunc (ww *WrittenResponseWriter) IsWritten() bool {\n\treturn ww.written\n}\n\nfunc GenerateDownProxyURL(storage *model.Storage, reqPath string) string {\n\tif storage.DownProxyURL == \"\" {\n\t\treturn \"\"\n\t}\n\tquery := \"\"\n\tif !storage.DisableProxySign {\n\t\tquery = \"?sign=\" + sign.Sign(reqPath)\n\t}\n\treturn fmt.Sprintf(\"%s%s%s\",\n\t\tstrings.Split(storage.DownProxyURL, \"\\n\")[0],\n\t\tutils.EncodePath(reqPath, true),\n\t\tquery,\n\t)\n}\n"
  },
  {
    "path": "server/common/resp.go",
    "content": "package common\n\ntype Resp[T any] struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    T      `json:\"data\"`\n}\n\ntype PageResp struct {\n\tContent interface{} `json:\"content\"`\n\tTotal   int64       `json:\"total\"`\n}\n"
  },
  {
    "path": "server/common/sign.go",
    "content": "package common\n\nimport (\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n)\n\nfunc Sign(obj model.Obj, parent string, encrypt bool) string {\n\tif obj.IsDir() || (!encrypt && !setting.GetBool(conf.SignAll)) {\n\t\treturn \"\"\n\t}\n\treturn sign.Sign(stdpath.Join(parent, obj.GetName()))\n}\n"
  },
  {
    "path": "server/debug.go",
    "content": "package server\n\nimport (\n\t\"net/http\"\n\t_ \"net/http/pprof\"\n\t\"runtime\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/middlewares\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc _pprof(g *gin.RouterGroup) {\n\tg.Any(\"/*name\", gin.WrapH(http.DefaultServeMux))\n}\n\nfunc debug(g *gin.RouterGroup) {\n\tg.GET(\"/path/*path\", middlewares.Down(sign.Verify), func(c *gin.Context) {\n\t\trawPath := c.Request.Context().Value(conf.PathKey).(string)\n\t\tc.JSON(200, gin.H{\n\t\t\t\"path\": rawPath,\n\t\t})\n\t})\n\tg.GET(\"/hide_privacy\", func(c *gin.Context) {\n\t\tcommon.ErrorStrResp(c, \"This is ip: 1.1.1.1\", 400)\n\t})\n\tg.GET(\"/gc\", func(c *gin.Context) {\n\t\truntime.GC()\n\t\tc.String(http.StatusOK, \"ok\")\n\t})\n\t_pprof(g.Group(\"/pprof\"))\n}\n"
  },
  {
    "path": "server/ftp/afero.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/spf13/afero\"\n)\n\ntype AferoAdapter struct {\n\tctx          context.Context\n\tnextFileSize int64\n}\n\nfunc NewAferoAdapter(ctx context.Context) *AferoAdapter {\n\treturn &AferoAdapter{ctx: ctx}\n}\n\nfunc (a *AferoAdapter) Create(_ string) (afero.File, error) {\n\t// See also GetHandle\n\treturn nil, errs.NotImplement\n}\n\nfunc (a *AferoAdapter) Mkdir(name string, _ os.FileMode) error {\n\treturn Mkdir(a.ctx, name)\n}\n\nfunc (a *AferoAdapter) MkdirAll(path string, perm os.FileMode) error {\n\treturn a.Mkdir(path, perm)\n}\n\nfunc (a *AferoAdapter) Open(_ string) (afero.File, error) {\n\t// See also GetHandle and ReadDir\n\treturn nil, errs.NotImplement\n}\n\nfunc (a *AferoAdapter) OpenFile(_ string, _ int, _ os.FileMode) (afero.File, error) {\n\t// See also GetHandle\n\treturn nil, errs.NotImplement\n}\n\nfunc (a *AferoAdapter) Remove(name string) error {\n\treturn Remove(a.ctx, name)\n}\n\nfunc (a *AferoAdapter) RemoveAll(path string) error {\n\treturn a.Remove(path)\n}\n\nfunc (a *AferoAdapter) Rename(oldName, newName string) error {\n\treturn Rename(a.ctx, oldName, newName)\n}\n\nfunc (a *AferoAdapter) Stat(name string) (os.FileInfo, error) {\n\treturn Stat(a.ctx, name)\n}\n\nfunc (a *AferoAdapter) Name() string {\n\treturn \"OpenList FTP Endpoint\"\n}\n\nfunc (a *AferoAdapter) Chmod(_ string, _ os.FileMode) error {\n\treturn errs.NotSupport\n}\n\nfunc (a *AferoAdapter) Chown(_ string, _, _ int) error {\n\treturn errs.NotSupport\n}\n\nfunc (a *AferoAdapter) Chtimes(_ string, _ time.Time, _ time.Time) error {\n\treturn errs.NotSupport\n}\n\nfunc (a *AferoAdapter) ReadDir(name string) ([]os.FileInfo, error) {\n\treturn List(a.ctx, name)\n}\n\nfunc (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {\n\tfileSize := a.nextFileSize\n\ta.nextFileSize = 0\n\tif (flags & os.O_SYNC) != 0 {\n\t\treturn nil, errs.NotSupport\n\t}\n\tif (flags & os.O_APPEND) != 0 {\n\t\treturn nil, errs.NotSupport\n\t}\n\tuser := a.ctx.Value(conf.UserKey).(*model.User)\n\tpath, err := user.JoinPath(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif f, err := Borrow(a.ctx, path); !errors.Is(err, errs.ObjectNotFound) {\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif (flags & os.O_EXCL) != 0 {\n\t\t\treturn nil, errs.ObjectAlreadyExists\n\t\t}\n\t\tif (flags & os.O_WRONLY) != 0 {\n\t\t\treturn nil, errors.New(\"cannot write to uploading file\")\n\t\t}\n\t\t_, err = f.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"failed seek borrow: %+v\", err)\n\t\t}\n\t\treturn f, nil\n\t}\n\t_, err = fs.Get(a.ctx, path, &fs.GetArgs{})\n\texists := err == nil\n\tif (flags&os.O_CREATE) == 0 && !exists {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tif (flags&os.O_EXCL) != 0 && exists {\n\t\treturn nil, errs.ObjectAlreadyExists\n\t}\n\tif (flags & os.O_WRONLY) != 0 {\n\t\tif offset != 0 {\n\t\t\treturn nil, errs.NotSupport\n\t\t}\n\t\ttrunc := (flags & os.O_TRUNC) != 0\n\t\tif fileSize > 0 {\n\t\t\treturn OpenUploadWithLength(a.ctx, path, trunc, fileSize)\n\t\t} else {\n\t\t\treturn OpenUpload(a.ctx, path, trunc)\n\t\t}\n\t}\n\treturn OpenDownload(a.ctx, path, offset)\n}\n\nfunc (a *AferoAdapter) Site(param string) *ftpserver.AnswerCommand {\n\tspl := strings.SplitN(param, \" \", 2)\n\tcmd := strings.ToUpper(spl[0])\n\tvar params string\n\tif len(spl) > 1 {\n\t\tparams = spl[1]\n\t} else {\n\t\tparams = \"\"\n\t}\n\tswitch cmd {\n\tcase \"SIZE\":\n\t\tcode, msg := HandleSIZE(params, a)\n\t\treturn &ftpserver.AnswerCommand{\n\t\t\tCode:    code,\n\t\t\tMessage: msg,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *AferoAdapter) SetNextFileSize(size int64) {\n\ta.nextFileSize = size\n}\n"
  },
  {
    "path": "server/ftp/fsmanage.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc Mkdir(ctx context.Context, path string) error {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !user.CanWrite() || !user.CanFTPManage() {\n\t\tmeta, err := op.GetNearestMeta(stdpath.Dir(reqPath))\n\t\tif err != nil {\n\t\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif !common.CanWrite(meta, reqPath) {\n\t\t\treturn errs.PermissionDenied\n\t\t}\n\t}\n\treturn fs.MakeDir(ctx, reqPath)\n}\n\nfunc Remove(ctx context.Context, path string) error {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tif !user.CanRemove() || !user.CanFTPManage() {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err = RemoveStage(reqPath); !errors.Is(err, errs.ObjectNotFound) {\n\t\treturn err\n\t}\n\treturn fs.Remove(ctx, reqPath)\n}\n\nfunc Rename(ctx context.Context, oldPath, newPath string) error {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tsrcPath, err := user.JoinPath(oldPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstPath, err := user.JoinPath(newPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsrcDir, srcBase := stdpath.Split(srcPath)\n\tdstDir, dstBase := stdpath.Split(dstPath)\n\tif srcDir == dstDir {\n\t\tif !user.CanRename() || !user.CanFTPManage() {\n\t\t\treturn errs.PermissionDenied\n\t\t}\n\t\tif err = MoveStage(srcPath, dstPath); !errors.Is(err, errs.ObjectNotFound) {\n\t\t\treturn err\n\t\t}\n\t\treturn fs.Rename(ctx, srcPath, dstBase)\n\t} else {\n\t\tif !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {\n\t\t\treturn errs.PermissionDenied\n\t\t}\n\t\tif err = MoveStage(srcPath, dstPath); !errors.Is(err, errs.ObjectNotFound) {\n\t\t\treturn err\n\t\t}\n\t\tif srcBase != dstBase {\n\t\t\terr = fs.Rename(ctx, srcPath, dstBase, true)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t_, err = fs.Move(ctx, stdpath.Join(srcDir, dstBase), dstDir)\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "server/ftp/fsread.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"io\"\n\tfs2 \"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/pkg/errors\"\n)\n\ntype FileDownloadProxy struct {\n\tmodel.File\n\tio.Closer\n\tctx context.Context\n}\n\nfunc OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownloadProxy, error) {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tctx = context.WithValue(ctx, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\n\t// directly use proxy\n\theader, _ := ctx.Value(conf.ProxyHeaderKey).(http.Header)\n\tip, _ := ctx.Value(conf.ClientIPKey).(string)\n\tlink, obj, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: ip, Header: header})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tss, err := stream.NewSeekableStream(&stream.FileStream{\n\t\tObj: obj,\n\t\tCtx: ctx,\n\t}, link)\n\tif err != nil {\n\t\t_ = link.Close()\n\t\treturn nil, err\n\t}\n\treader, err := stream.NewReadAtSeeker(ss, offset)\n\tif err != nil {\n\t\t_ = ss.Close()\n\t\treturn nil, err\n\t}\n\treturn &FileDownloadProxy{File: reader, Closer: ss, ctx: ctx}, nil\n}\n\nfunc (f *FileDownloadProxy) Read(p []byte) (n int, err error) {\n\tn, err = f.File.Read(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientDownloadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *FileDownloadProxy) ReadAt(p []byte, off int64) (n int, err error) {\n\tn, err = f.File.ReadAt(p, off)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientDownloadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *FileDownloadProxy) Write(p []byte) (n int, err error) {\n\treturn 0, errs.NotSupport\n}\n\ntype OsFileInfoAdapter struct {\n\tobj model.Obj\n}\n\nfunc (o *OsFileInfoAdapter) Name() string {\n\treturn o.obj.GetName()\n}\n\nfunc (o *OsFileInfoAdapter) Size() int64 {\n\treturn o.obj.GetSize()\n}\n\nfunc (o *OsFileInfoAdapter) Mode() fs2.FileMode {\n\tvar mode fs2.FileMode = 0o755\n\tif o.IsDir() {\n\t\tmode |= fs2.ModeDir\n\t}\n\treturn mode\n}\n\nfunc (o *OsFileInfoAdapter) ModTime() time.Time {\n\treturn o.obj.ModTime()\n}\n\nfunc (o *OsFileInfoAdapter) IsDir() bool {\n\treturn o.obj.IsDir()\n}\n\nfunc (o *OsFileInfoAdapter) Sys() any {\n\treturn o.obj\n}\n\nfunc Stat(ctx context.Context, path string) (os.FileInfo, error) {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tctx = context.WithValue(ctx, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\tif ret, err := StatStage(reqPath); !errors.Is(err, errs.ObjectNotFound) {\n\t\treturn ret, err\n\t}\n\tobj, err := fs.Get(ctx, reqPath, &fs.GetArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &OsFileInfoAdapter{obj: obj}, nil\n}\n\nfunc List(ctx context.Context, path string) ([]os.FileInfo, error) {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tctx = context.WithValue(ctx, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\tobjs, err := fs.List(ctx, reqPath, &fs.ListArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuploading := ListStage(reqPath)\n\tfor _, o := range objs {\n\t\tdelete(uploading, o.GetName())\n\t}\n\tfor _, u := range uploading {\n\t\tobjs = append(objs, u)\n\t}\n\tret := make([]os.FileInfo, len(objs))\n\tfor i, obj := range objs {\n\t\tret[i] = &OsFileInfoAdapter{obj: obj}\n\t}\n\treturn ret, nil\n}\n"
  },
  {
    "path": "server/ftp/fsup.go",
    "content": "package ftp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/pkg/errors\"\n)\n\ntype FileUploadProxy struct {\n\tftpserver.FileTransfer\n\tbuffer *os.File\n\tpath   string\n\tctx    context.Context\n\ttrunc  bool\n}\n\nfunc uploadAuth(ctx context.Context, path string) error {\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tmeta, err := op.GetNearestMeta(stdpath.Dir(path))\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !(common.CanAccess(user, meta, path, ctx.Value(conf.MetaPassKey).(string)) &&\n\t\t((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {\n\t\treturn errs.PermissionDenied\n\t}\n\treturn nil\n}\n\nfunc OpenUpload(ctx context.Context, path string, trunc bool) (*FileUploadProxy, error) {\n\terr := uploadAuth(ctx, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Check if system file should be ignored\n\t_, name := stdpath.Split(path)\n\tif setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(name) {\n\t\treturn nil, errs.IgnoredSystemFile\n\t}\n\ttmpFile, err := os.CreateTemp(conf.Conf.TempDir, \"file-*\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx, trunc: trunc}, nil\n}\n\nfunc (f *FileUploadProxy) Read(p []byte) (n int, err error) {\n\treturn 0, errs.NotSupport\n}\n\nfunc (f *FileUploadProxy) Write(p []byte) (n int, err error) {\n\tn, err = f.buffer.Write(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientUploadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *FileUploadProxy) Seek(offset int64, whence int) (int64, error) {\n\treturn f.buffer.Seek(offset, whence)\n}\n\nfunc (f *FileUploadProxy) Close() error {\n\tdir, name := stdpath.Split(f.path)\n\tsize, err := f.buffer.Seek(0, io.SeekCurrent)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := f.buffer.Seek(0, io.SeekStart); err != nil {\n\t\treturn err\n\t}\n\tarr := make([]byte, 512)\n\tif _, err := f.buffer.Read(arr); err != nil {\n\t\treturn err\n\t}\n\tcontentType := http.DetectContentType(arr)\n\tif _, err := f.buffer.Seek(0, io.SeekStart); err != nil {\n\t\treturn err\n\t}\n\tuser := f.ctx.Value(conf.UserKey).(*model.User)\n\tsf, borrow, err := MakeStage(f.ctx, f.buffer, size, f.path, func(target string) {\n\t\tctx := context.WithValue(context.Background(), conf.UserKey, user)\n\t\tdstDir, dstBase := stdpath.Split(target)\n\t\tif dir == dstDir {\n\t\t\t_ = fs.Rename(ctx, f.path, dstBase)\n\t\t} else {\n\t\t\tif name != dstBase {\n\t\t\t\te := fs.Rename(ctx, f.path, dstBase, true)\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t_, _ = fs.Move(ctx, stdpath.Join(dir, dstBase), dstDir)\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed make stage for [%s]: %+v\", f.path, err)\n\t}\n\tif f.trunc {\n\t\t_ = fs.Remove(f.ctx, f.path)\n\t}\n\ts := &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tName:     name,\n\t\t\tSize:     size,\n\t\t\tModified: time.Now(),\n\t\t},\n\t\tMimetype:     contentType,\n\t\tWebPutAsTask: true,\n\t\tReader:       f.buffer,\n\t}\n\ts.Add(borrow)\n\ttask, err := fs.PutAsTask(f.ctx, dir, s)\n\tif err != nil {\n\t\t_ = s.Close()\n\t\treturn err\n\t}\n\tsf.SetRemoveCallback(func() {\n\t\tfs.UploadTaskManager.Cancel(task.GetID())\n\t})\n\treturn nil\n}\n\ntype FileUploadWithLengthProxy struct {\n\tftpserver.FileTransfer\n\tctx           context.Context\n\tpath          string\n\tlength        int64\n\tfirst512Bytes [512]byte\n\tpFirst        int\n\tpipeWriter    io.WriteCloser\n\terrChan       chan error\n}\n\nfunc OpenUploadWithLength(ctx context.Context, path string, trunc bool, length int64) (*FileUploadWithLengthProxy, error) {\n\terr := uploadAuth(ctx, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Check if system file should be ignored\n\t_, name := stdpath.Split(path)\n\tif setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(name) {\n\t\treturn nil, errs.IgnoredSystemFile\n\t}\n\tif trunc {\n\t\t_ = fs.Remove(ctx, path)\n\t}\n\treturn &FileUploadWithLengthProxy{ctx: ctx, path: path, length: length}, nil\n}\n\nfunc (f *FileUploadWithLengthProxy) Read(p []byte) (n int, err error) {\n\treturn 0, errs.NotSupport\n}\n\nfunc (f *FileUploadWithLengthProxy) write(p []byte) (n int, err error) {\n\tif f.pipeWriter != nil {\n\t\tselect {\n\t\tcase e := <-f.errChan:\n\t\t\treturn 0, e\n\t\tdefault:\n\t\t\treturn f.pipeWriter.Write(p)\n\t\t}\n\t} else if len(p) < 512-f.pFirst {\n\t\tcopy(f.first512Bytes[f.pFirst:], p)\n\t\tf.pFirst += len(p)\n\t\treturn len(p), nil\n\t} else {\n\t\tcopy(f.first512Bytes[f.pFirst:], p[:512-f.pFirst])\n\t\tcontentType := http.DetectContentType(f.first512Bytes[:])\n\t\tdir, name := stdpath.Split(f.path)\n\t\treader, writer := io.Pipe()\n\t\tf.errChan = make(chan error, 1)\n\t\ts := &stream.FileStream{\n\t\t\tObj: &model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tSize:     f.length,\n\t\t\t\tModified: time.Now(),\n\t\t\t},\n\t\t\tMimetype:     contentType,\n\t\t\tWebPutAsTask: false,\n\t\t\tReader:       reader,\n\t\t}\n\t\tgo func() {\n\t\t\te := fs.PutDirectly(f.ctx, dir, s, true)\n\t\t\tf.errChan <- e\n\t\t\tclose(f.errChan)\n\t\t}()\n\t\tf.pipeWriter = writer\n\t\tn, err = writer.Write(f.first512Bytes[:])\n\t\tif err != nil {\n\t\t\treturn n, err\n\t\t}\n\t\tn1, err := writer.Write(p[512-f.pFirst:])\n\t\tif err != nil {\n\t\t\treturn n1 + 512 - f.pFirst, err\n\t\t}\n\t\tf.pFirst = 512\n\t\treturn len(p), nil\n\t}\n}\n\nfunc (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) {\n\tn, err = f.write(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientUploadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) {\n\treturn 0, errs.NotSupport\n}\n\nfunc (f *FileUploadWithLengthProxy) Close() error {\n\tif f.pipeWriter != nil {\n\t\terr := f.pipeWriter.Close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = <-f.errChan\n\t\treturn err\n\t} else {\n\t\tdata := f.first512Bytes[:f.pFirst]\n\t\tcontentType := http.DetectContentType(data)\n\t\tdir, name := stdpath.Split(f.path)\n\t\ts := &stream.FileStream{\n\t\t\tObj: &model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tSize:     int64(f.pFirst),\n\t\t\t\tModified: time.Now(),\n\t\t\t},\n\t\t\tMimetype:     contentType,\n\t\t\tWebPutAsTask: false,\n\t\t\tReader:       bytes.NewReader(data),\n\t\t}\n\t\treturn fs.PutDirectly(f.ctx, dir, s)\n\t}\n}\n"
  },
  {
    "path": "server/ftp/site.go",
    "content": "package ftp\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n)\n\nfunc HandleSIZE(param string, client ftpserver.ClientDriver) (int, string) {\n\tfs, ok := client.(*AferoAdapter)\n\tif !ok {\n\t\treturn ftpserver.StatusNotLoggedIn, \"Unexpected exception (driver is nil)\"\n\t}\n\tsize, err := strconv.ParseInt(param, 10, 64)\n\tif err != nil {\n\t\treturn ftpserver.StatusSyntaxErrorParameters, fmt.Sprintf(\n\t\t\t\"Couldn't parse file size, given: %s, err: %v\", param, err)\n\t}\n\tfs.SetNextFileSize(size)\n\treturn ftpserver.StatusOK, \"Accepted next file size\"\n}\n"
  },
  {
    "path": "server/ftp/upload_stage.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tchap/go-patricia/v2/patricia\"\n)\n\nvar (\n\tstage                *patricia.Trie\n\tstageMutex           = sync.Mutex{}\n\tErrStagePathConflict = errors.New(\"upload path conflict\")\n\tErrStageMoved        = errors.New(\"uploading file has been moved\")\n)\n\nfunc InitStage() {\n\tif stage != nil {\n\t\treturn\n\t}\n\tstage = patricia.NewTrie(patricia.MaxPrefixPerNode(16))\n}\n\ntype UploadingFile struct {\n\tname        string\n\tsize        int64\n\tmodTime     time.Time\n\trefCount    int\n\tcurrentPath string\n\tsoftLinks   []patricia.Prefix\n\tmvCallback  func(string)\n\trmCallback  func()\n}\n\nfunc (u *UploadingFile) SetRemoveCallback(rm func()) {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tu.rmCallback = rm\n}\n\ntype softLink struct {\n\ttarget *UploadingFile\n}\n\nfunc MakeStage(ctx context.Context, buffer *os.File, size int64, path string, mv func(string)) (*UploadingFile, *BorrowedFile, error) {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tprefix := patricia.Prefix(path)\n\tf := &UploadingFile{\n\t\tname:        buffer.Name(),\n\t\tsize:        size,\n\t\tmodTime:     time.Now(),\n\t\trefCount:    1,\n\t\tcurrentPath: path,\n\t\tsoftLinks:   []patricia.Prefix{},\n\t\tmvCallback:  mv,\n\t}\n\tif !stage.Insert(prefix, f) {\n\t\treturn nil, nil, ErrStagePathConflict\n\t}\n\tlog.Debugf(\"[ftp-stage] succeed to make [%s] stage\", buffer.Name())\n\treturn f, &BorrowedFile{\n\t\tfile: buffer,\n\t\tpath: prefix,\n\t\tctx:  ctx,\n\t}, nil\n}\n\nfunc Borrow(ctx context.Context, path string) (*BorrowedFile, error) {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tprefix := patricia.Prefix(path)\n\tv := stage.Get(prefix)\n\tif v == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\ts, ok := v.(*UploadingFile)\n\tif !ok {\n\t\ts = v.(*softLink).target\n\t}\n\tif s.currentPath != path {\n\t\treturn nil, ErrStageMoved\n\t}\n\tborrowed, err := os.OpenFile(s.name, os.O_RDONLY, 0o644)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed borrow [%s]: %+v\", s.name, err)\n\t}\n\ts.refCount++\n\tlog.Debugf(\"[ftp-stage] borrow [%s] succeed\", s.name)\n\treturn &BorrowedFile{\n\t\tfile: borrowed,\n\t\tpath: prefix,\n\t\tctx:  ctx,\n\t}, nil\n}\n\nfunc drop(path patricia.Prefix) {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tv := stage.Get(path)\n\tif v == nil {\n\t\treturn\n\t}\n\ts, ok := v.(*UploadingFile)\n\tif !ok {\n\t\ts = v.(*softLink).target\n\t}\n\ts.refCount--\n\tlog.Debugf(\"[ftp-stage] dropped [%s]\", s.name)\n\tif s.refCount == 0 {\n\t\tlog.Debugf(\"[ftp-stage] there is no more reference to [%s], removing temp file\", s.name)\n\t\terr := os.RemoveAll(s.name)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"[ftp-stage] failed to remove stage file [%s]: %+v\", s.name, err)\n\t\t}\n\t\tfor _, sl := range s.softLinks {\n\t\t\tstage.Delete(sl)\n\t\t}\n\t\tstage.Delete(path)\n\t\tif s.currentPath != string(path) {\n\t\t\tif s.currentPath != \"\" {\n\t\t\t\tgo s.mvCallback(s.currentPath)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc ListStage(path string) map[string]model.Obj {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tpath = path + \"/\"\n\tprefix := patricia.Prefix(path)\n\tret := make(map[string]model.Obj)\n\t_ = stage.VisitSubtree(prefix, func(prefix patricia.Prefix, item patricia.Item) error {\n\t\tvisit := string(prefix)\n\t\tvisitSub := strings.TrimPrefix(visit, path)\n\t\tname, _, nonDirect := strings.Cut(visitSub, \"/\")\n\t\tif nonDirect {\n\t\t\treturn nil\n\t\t}\n\t\tf, ok := item.(*UploadingFile)\n\t\tif !ok {\n\t\t\tf = item.(*softLink).target\n\t\t}\n\t\tif f.currentPath == visit {\n\t\t\tret[name] = &model.Object{\n\t\t\t\tPath:     visit,\n\t\t\t\tName:     name,\n\t\t\t\tSize:     f.size,\n\t\t\t\tModified: f.modTime,\n\t\t\t\tIsFolder: false,\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn ret\n}\n\nfunc StatStage(path string) (os.FileInfo, error) {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tprefix := patricia.Prefix(path)\n\tv := stage.Get(prefix)\n\tif v == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\ts, ok := v.(*UploadingFile)\n\tif !ok {\n\t\ts = v.(*softLink).target\n\t}\n\tif s.currentPath != path {\n\t\treturn nil, ErrStageMoved\n\t}\n\treturn os.Stat(s.name)\n}\n\nfunc MoveStage(from, to string) error {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tprefix := patricia.Prefix(from)\n\tv := stage.Get(prefix)\n\tif v == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\ts, ok := v.(*UploadingFile)\n\tif !ok {\n\t\ts = v.(*softLink).target\n\t}\n\tif s.currentPath != from {\n\t\treturn ErrStageMoved\n\t}\n\tslPrefix := patricia.Prefix(to)\n\tsl := &softLink{target: s}\n\tif !stage.Insert(slPrefix, sl) {\n\t\treturn ErrStagePathConflict\n\t}\n\ts.currentPath = to\n\ts.softLinks = append(s.softLinks, slPrefix)\n\treturn nil\n}\n\nfunc RemoveStage(path string) error {\n\tstageMutex.Lock()\n\tdefer stageMutex.Unlock()\n\tprefix := patricia.Prefix(path)\n\tv := stage.Get(prefix)\n\tif v == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\ts, ok := v.(*UploadingFile)\n\tif !ok {\n\t\ts = v.(*softLink).target\n\t}\n\tif s.currentPath != path {\n\t\treturn ErrStageMoved\n\t}\n\ts.currentPath = \"\"\n\tif s.rmCallback != nil {\n\t\ts.rmCallback()\n\t}\n\treturn nil\n}\n\ntype BorrowedFile struct {\n\tfile *os.File\n\tpath patricia.Prefix\n\tctx  context.Context\n}\n\nfunc (f *BorrowedFile) Read(p []byte) (n int, err error) {\n\tn, err = f.file.Read(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientDownloadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *BorrowedFile) ReadAt(p []byte, off int64) (n int, err error) {\n\tn, err = f.file.ReadAt(p, off)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\terr = stream.ClientDownloadLimit.WaitN(f.ctx, n)\n\treturn n, err\n}\n\nfunc (f *BorrowedFile) Seek(offset int64, whence int) (int64, error) {\n\treturn f.file.Seek(offset, whence)\n}\n\nfunc (f *BorrowedFile) Write(_ []byte) (n int, err error) {\n\treturn 0, errs.NotSupport\n}\n\nfunc (f *BorrowedFile) Close() error {\n\terr := f.file.Close()\n\tdrop(f.path)\n\treturn err\n}\n"
  },
  {
    "path": "server/ftp.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/ftp\"\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n)\n\ntype FtpMainDriver struct {\n\tsettings     *ftpserver.Settings\n\tproxyHeader  http.Header\n\tclients      map[uint32]ftpserver.ClientContext\n\tshutdownLock sync.RWMutex\n\tisShutdown   bool\n\ttlsConfig    *tls.Config\n}\n\nfunc NewMainDriver() (*FtpMainDriver, error) {\n\tftp.InitStage()\n\ttransferType := ftpserver.TransferTypeASCII\n\tif conf.Conf.FTP.DefaultTransferBinary {\n\t\ttransferType = ftpserver.TransferTypeBinary\n\t}\n\tactiveConnCheck := ftpserver.IPMatchDisabled\n\tif conf.Conf.FTP.EnableActiveConnIPCheck {\n\t\tactiveConnCheck = ftpserver.IPMatchRequired\n\t}\n\tpasvConnCheck := ftpserver.IPMatchDisabled\n\tif conf.Conf.FTP.EnablePasvConnIPCheck {\n\t\tpasvConnCheck = ftpserver.IPMatchRequired\n\t}\n\ttlsRequired := ftpserver.ClearOrEncrypted\n\tif setting.GetBool(conf.FTPImplicitTLS) {\n\t\ttlsRequired = ftpserver.ImplicitEncryption\n\t} else if setting.GetBool(conf.FTPMandatoryTLS) {\n\t\ttlsRequired = ftpserver.MandatoryEncryption\n\t}\n\ttlsConf, err := getTlsConf(setting.GetStr(conf.FTPTLSPrivateKeyPath), setting.GetStr(conf.FTPTLSPublicCertPath))\n\tif err != nil && tlsRequired != ftpserver.ClearOrEncrypted {\n\t\treturn nil, fmt.Errorf(\"FTP mandatory TLS has been enabled, but the certificate failed to load: %w\", err)\n\t}\n\treturn &FtpMainDriver{\n\t\tsettings: &ftpserver.Settings{\n\t\t\tListenAddr:               conf.Conf.FTP.Listen,\n\t\t\tPublicHost:               lookupIP(setting.GetStr(conf.FTPPublicHost)),\n\t\t\tPassiveTransferPortRange: newPortMapper(setting.GetStr(conf.FTPPasvPortMap)),\n\t\t\tActiveTransferPortNon20:  conf.Conf.FTP.ActiveTransferPortNon20,\n\t\t\tIdleTimeout:              conf.Conf.FTP.IdleTimeout,\n\t\t\tConnectionTimeout:        conf.Conf.FTP.ConnectionTimeout,\n\t\t\tDisableMLSD:              false,\n\t\t\tDisableMLST:              false,\n\t\t\tDisableMFMT:              true,\n\t\t\tBanner:                   setting.GetStr(conf.Announcement),\n\t\t\tTLSRequired:              tlsRequired,\n\t\t\tDisableLISTArgs:          false,\n\t\t\tDisableSite:              false,\n\t\t\tDisableActiveMode:        conf.Conf.FTP.DisableActiveMode,\n\t\t\tEnableHASH:               false,\n\t\t\tDisableSTAT:              false,\n\t\t\tDisableSYST:              false,\n\t\t\tEnableCOMB:               false,\n\t\t\tDefaultTransferType:      transferType,\n\t\t\tActiveConnectionsCheck:   activeConnCheck,\n\t\t\tPasvConnectionsCheck:     pasvConnCheck,\n\t\t},\n\t\tproxyHeader: http.Header{\n\t\t\t\"User-Agent\": {base.UserAgent},\n\t\t},\n\t\tclients:      make(map[uint32]ftpserver.ClientContext),\n\t\tshutdownLock: sync.RWMutex{},\n\t\tisShutdown:   false,\n\t\ttlsConfig:    tlsConf,\n\t}, nil\n}\n\nfunc (d *FtpMainDriver) GetSettings() (*ftpserver.Settings, error) {\n\treturn d.settings, nil\n}\n\nfunc (d *FtpMainDriver) ClientConnected(cc ftpserver.ClientContext) (string, error) {\n\tif d.isShutdown || !d.shutdownLock.TryRLock() {\n\t\treturn \"\", errors.New(\"server has shutdown\")\n\t}\n\tdefer d.shutdownLock.RUnlock()\n\td.clients[cc.ID()] = cc\n\treturn \"OpenList FTP Endpoint\", nil\n}\n\nfunc (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {\n\terr := cc.Close()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to close client: %v\", err)\n\t}\n\tdelete(d.clients, cc.ID())\n}\n\nfunc (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {\n\tip := cc.RemoteAddr().String()\n\tcount, ok := model.LoginCache.Get(ip)\n\tif ok && count >= model.DefaultMaxAuthRetries {\n\t\tmodel.LoginCache.Expire(ip, model.DefaultLockDuration)\n\t\treturn nil, errors.New(\"Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.\")\n\t}\n\tvar userObj *model.User\n\tvar err error\n\tif user == \"anonymous\" || user == \"guest\" {\n\t\tuserObj, err = op.GetGuest()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tuserObj, err = op.GetUserByName(user)\n\t\tif err == nil {\n\t\t\terr = userObj.ValidateRawPassword(pass)\n\t\t\tif err != nil && setting.GetBool(conf.LdapLoginEnabled) && userObj.AllowLdap {\n\t\t\t\terr = common.HandleLdapLogin(user, pass)\n\t\t\t}\n\t\t} else if setting.GetBool(conf.LdapLoginEnabled) && model.CanFTPAccess(int32(setting.GetInt(conf.LdapDefaultPermission, 0))) {\n\t\t\tuserObj, err = tryLdapLoginAndRegister(user, pass)\n\t\t}\n\t\tif err != nil {\n\t\t\tmodel.LoginCache.Set(ip, count+1)\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif userObj.Disabled || !userObj.CanFTPAccess() {\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\treturn nil, errors.New(\"user is not allowed to access via FTP\")\n\t}\n\tmodel.LoginCache.Del(ip)\n\n\tctx := context.Background()\n\tctx = context.WithValue(ctx, conf.UserKey, userObj)\n\tif user == \"anonymous\" || user == \"guest\" {\n\t\tctx = context.WithValue(ctx, conf.MetaPassKey, pass)\n\t} else {\n\t\tctx = context.WithValue(ctx, conf.MetaPassKey, \"\")\n\t}\n\tctx = context.WithValue(ctx, conf.ClientIPKey, ip)\n\tctx = context.WithValue(ctx, conf.ProxyHeaderKey, d.proxyHeader)\n\treturn ftp.NewAferoAdapter(ctx), nil\n}\n\nfunc (d *FtpMainDriver) GetTLSConfig() (*tls.Config, error) {\n\tif d.tlsConfig == nil {\n\t\treturn nil, errors.New(\"TLS config not provided\")\n\t}\n\treturn d.tlsConfig, nil\n}\n\nfunc (d *FtpMainDriver) Stop() {\n\td.isShutdown = true\n\td.shutdownLock.Lock()\n\tdefer d.shutdownLock.Unlock()\n\tfor _, value := range d.clients {\n\t\t_ = value.Close()\n\t}\n}\n\nfunc lookupIP(host string) string {\n\tif host == \"\" || net.ParseIP(host) != nil {\n\t\treturn host\n\t}\n\tips, err := net.LookupIP(host)\n\tif err != nil || len(ips) == 0 {\n\t\tutils.Log.Errorf(\"given FTP public host is invalid, and the default value will be used: %v\", err)\n\t\treturn \"\"\n\t}\n\tfor _, ip := range ips {\n\t\tif ip.To4() != nil {\n\t\t\treturn ip.String()\n\t\t}\n\t}\n\tv6 := ips[0].String()\n\tutils.Log.Warnf(\"no IPv4 record looked up, %s will be used as public host, and it might do not work.\", v6)\n\treturn v6\n}\n\ntype group struct {\n\tExposedStart  int\n\tListenedStart int\n\tLength        int\n}\n\ntype pasvPortGetter struct {\n\tgroups      []group\n\ttotalLength int\n}\n\nfunc (m *pasvPortGetter) FetchNext() (int, int, bool) {\n\tidxPort := rand.Intn(m.totalLength)\n\tfor _, g := range m.groups {\n\t\tif idxPort >= g.Length {\n\t\t\tidxPort -= g.Length\n\t\t} else {\n\t\t\treturn g.ExposedStart + idxPort, g.ListenedStart + idxPort, true\n\t\t}\n\t}\n\t// unreachable\n\treturn 0, 0, false\n}\n\nfunc (m *pasvPortGetter) NumberAttempts() int {\n\treturn conf.Conf.FTP.FindPasvPortAttempts\n}\n\nfunc newPortMapper(str string) ftpserver.PasvPortGetter {\n\tif str == \"\" {\n\t\treturn nil\n\t}\n\tpasvPortMappers := strings.Split(strings.Replace(str, \"\\n\", \",\", -1), \",\")\n\tgroups := make([]group, len(pasvPortMappers))\n\ttotalLength := 0\n\tconvertToPorts := func(str string) (int, int, error) {\n\t\tstart, end, multi := strings.Cut(str, \"-\")\n\t\tif multi {\n\t\t\tsi, err := strconv.Atoi(start)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\t\t\tei, err := strconv.Atoi(end)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\t\t\tif ei < si || ei < 1024 || si < 1024 || ei > 65535 || si > 65535 {\n\t\t\t\treturn 0, 0, errors.New(\"invalid port\")\n\t\t\t}\n\t\t\treturn si, ei - si + 1, nil\n\t\t} else {\n\t\t\tret, err := strconv.Atoi(str)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t} else {\n\t\t\t\treturn ret, 1, nil\n\t\t\t}\n\t\t}\n\t}\n\tfor i, mapper := range pasvPortMappers {\n\t\tvar err error\n\t\texposed, listened, mapped := strings.Cut(mapper, \":\")\n\t\tfor {\n\t\t\tif mapped {\n\t\t\t\tvar es, ls, el, ll int\n\t\t\t\tes, el, err = convertToPorts(exposed)\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tls, ll, err = convertToPorts(listened)\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif el != ll {\n\t\t\t\t\terr = errors.New(\"the number of exposed ports and listened ports does not match\")\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tgroups[i].ExposedStart = es\n\t\t\t\tgroups[i].ListenedStart = ls\n\t\t\t\tgroups[i].Length = el\n\t\t\t\ttotalLength += el\n\t\t\t} else {\n\t\t\t\tvar start, length int\n\t\t\t\tstart, length, err = convertToPorts(mapper)\n\t\t\t\tgroups[i].ExposedStart = start\n\t\t\t\tgroups[i].ListenedStart = start\n\t\t\t\tgroups[i].Length = length\n\t\t\t\ttotalLength += length\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to convert FTP PASV port mapper %s: %v, the port mapper will be ignored.\", mapper, err)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn &pasvPortGetter{groups: groups, totalLength: totalLength}\n}\n\nfunc getTlsConf(keyPath, certPath string) (*tls.Config, error) {\n\tif keyPath == \"\" || certPath == \"\" {\n\t\treturn nil, errors.New(\"private key or certificate is not provided\")\n\t}\n\tcert, err := os.ReadFile(certPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkey, err := os.ReadFile(keyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttlsCert, err := tls.X509KeyPair(cert, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &tls.Config{Certificates: []tls.Certificate{tlsCert}}, nil\n}\n"
  },
  {
    "path": "server/handles/archive.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/archive/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype ArchiveMetaReq struct {\n\tPath        string `json:\"path\" form:\"path\"`\n\tPassword    string `json:\"password\" form:\"password\"`\n\tRefresh     bool   `json:\"refresh\" form:\"refresh\"`\n\tArchivePass string `json:\"archive_pass\" form:\"archive_pass\"`\n}\n\ntype ArchiveMetaResp struct {\n\tComment     string               `json:\"comment\"`\n\tIsEncrypted bool                 `json:\"encrypted\"`\n\tContent     []ArchiveContentResp `json:\"content\"`\n\tSort        *model.Sort          `json:\"sort,omitempty\"`\n\tRawURL      string               `json:\"raw_url\"`\n\tSign        string               `json:\"sign\"`\n}\n\ntype ArchiveContentResp struct {\n\tObjResp\n\tChildren []ArchiveContentResp `json:\"children\"`\n}\n\nfunc toObjsRespWithoutSignAndThumb(obj model.Obj) ObjResp {\n\treturn ObjResp{\n\t\tName:        obj.GetName(),\n\t\tSize:        obj.GetSize(),\n\t\tIsDir:       obj.IsDir(),\n\t\tModified:    obj.ModTime(),\n\t\tCreated:     obj.CreateTime(),\n\t\tHashInfoStr: obj.GetHash().String(),\n\t\tHashInfo:    obj.GetHash().Export(),\n\t\tSign:        \"\",\n\t\tThumb:       \"\",\n\t\tType:        utils.GetObjType(obj.GetName(), obj.IsDir()),\n\t}\n}\n\nfunc toContentResp(objs []model.ObjTree) []ArchiveContentResp {\n\tif objs == nil {\n\t\treturn nil\n\t}\n\tret, _ := utils.SliceConvert(objs, func(src model.ObjTree) (ArchiveContentResp, error) {\n\t\treturn ArchiveContentResp{\n\t\t\tObjResp:  toObjsRespWithoutSignAndThumb(src),\n\t\t\tChildren: toContentResp(src.GetChildren()),\n\t\t}, nil\n\t})\n\treturn ret\n}\n\nfunc FsArchiveMetaSplit(c *gin.Context) {\n\tvar req ArchiveMetaReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif strings.HasPrefix(req.Path, \"/@s\") {\n\t\treq.Path = strings.TrimPrefix(req.Path, \"/@s\")\n\t\tSharingArchiveMeta(c, &req)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() && user.Disabled {\n\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\treturn\n\t}\n\tFsArchiveMeta(c, &req, user)\n}\n\nfunc FsArchiveMeta(c *gin.Context, req *ArchiveMetaReq, user *model.User) {\n\tif !user.CanReadArchives() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tarchiveArgs := model.ArchiveArgs{\n\t\tLinkArgs: model.LinkArgs{\n\t\t\tHeader: c.Request.Header,\n\t\t\tType:   c.Query(\"type\"),\n\t\t},\n\t\tPassword: req.ArchivePass,\n\t}\n\tret, err := fs.ArchiveMeta(c.Request.Context(), reqPath, model.ArchiveMetaArgs{\n\t\tArchiveArgs: archiveArgs,\n\t\tRefresh:     req.Refresh,\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, errs.WrongArchivePassword) {\n\t\t\tcommon.ErrorResp(c, err, 202)\n\t\t} else {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t}\n\t\treturn\n\t}\n\ts := \"\"\n\tif isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {\n\t\ts = sign.SignArchive(reqPath)\n\t}\n\tapi := \"/ae\"\n\tif ret.DriverProviding {\n\t\tapi = \"/ad\"\n\t}\n\tcommon.SuccessResp(c, ArchiveMetaResp{\n\t\tComment:     ret.GetComment(),\n\t\tIsEncrypted: ret.IsEncrypted(),\n\t\tContent:     toContentResp(ret.GetTree()),\n\t\tSort:        ret.Sort,\n\t\tRawURL:      fmt.Sprintf(\"%s%s%s\", common.GetApiUrl(c), api, utils.EncodePath(reqPath, true)),\n\t\tSign:        s,\n\t})\n}\n\ntype ArchiveListReq struct {\n\tArchiveMetaReq\n\tmodel.PageReq\n\tInnerPath string `json:\"inner_path\" form:\"inner_path\"`\n}\n\nfunc FsArchiveListSplit(c *gin.Context) {\n\tvar req ArchiveListReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tif strings.HasPrefix(req.Path, \"/@s\") {\n\t\treq.Path = strings.TrimPrefix(req.Path, \"/@s\")\n\t\tSharingArchiveList(c, &req)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() && user.Disabled {\n\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\treturn\n\t}\n\tFsArchiveList(c, &req, user)\n}\n\nfunc FsArchiveList(c *gin.Context, req *ArchiveListReq, user *model.User) {\n\tif !user.CanReadArchives() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tobjs, err := fs.ArchiveList(c.Request.Context(), reqPath, model.ArchiveListArgs{\n\t\tArchiveInnerArgs: model.ArchiveInnerArgs{\n\t\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\t\tHeader: c.Request.Header,\n\t\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t\t},\n\t\t\t\tPassword: req.ArchivePass,\n\t\t\t},\n\t\t\tInnerPath: utils.FixAndCleanPath(req.InnerPath),\n\t\t},\n\t\tRefresh: req.Refresh,\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, errs.WrongArchivePassword) {\n\t\t\tcommon.ErrorResp(c, err, 202)\n\t\t} else {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t}\n\t\treturn\n\t}\n\ttotal, objs := pagination(objs, &req.PageReq)\n\tret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) {\n\t\treturn toObjsRespWithoutSignAndThumb(src), nil\n\t})\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: ret,\n\t\tTotal:   int64(total),\n\t})\n}\n\ntype ArchiveDecompressReq struct {\n\tSrcDir        string   `json:\"src_dir\" form:\"src_dir\"`\n\tDstDir        string   `json:\"dst_dir\" form:\"dst_dir\"`\n\tNames         []string `json:\"name\" form:\"name\"`\n\tArchivePass   string   `json:\"archive_pass\" form:\"archive_pass\"`\n\tInnerPath     string   `json:\"inner_path\" form:\"inner_path\"`\n\tCacheFull     bool     `json:\"cache_full\" form:\"cache_full\"`\n\tPutIntoNewDir bool     `json:\"put_into_new_dir\" form:\"put_into_new_dir\"`\n\tOverwrite     bool     `json:\"overwrite\" form:\"overwrite\"`\n}\n\nfunc FsArchiveDecompress(c *gin.Context) {\n\tvar req ArchiveDecompressReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanDecompress() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tsrcPaths := make([]string, 0, len(req.Names))\n\tfor _, name := range req.Names {\n\t\tsrcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t\tsrcPaths = append(srcPaths, srcPath)\n\t}\n\tdstDir, err := user.JoinPath(req.DstDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\ttasks := make([]task.TaskExtensionInfo, 0, len(srcPaths))\n\tfor _, srcPath := range srcPaths {\n\t\tt, e := fs.ArchiveDecompress(c.Request.Context(), srcPath, dstDir, model.ArchiveDecompressArgs{\n\t\t\tArchiveInnerArgs: model.ArchiveInnerArgs{\n\t\t\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\t\t\tHeader: c.Request.Header,\n\t\t\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t\t\t},\n\t\t\t\t\tPassword: req.ArchivePass,\n\t\t\t\t},\n\t\t\t\tInnerPath: utils.FixAndCleanPath(req.InnerPath),\n\t\t\t},\n\t\t\tCacheFull:     req.CacheFull,\n\t\t\tPutIntoNewDir: req.PutIntoNewDir,\n\t\t\tOverwrite:     req.Overwrite,\n\t\t})\n\t\tif e != nil {\n\t\t\tif errors.Is(e, errs.WrongArchivePassword) {\n\t\t\t\tcommon.ErrorResp(c, e, 202)\n\t\t\t} else {\n\t\t\t\tcommon.ErrorResp(c, e, 500)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif t != nil {\n\t\t\ttasks = append(tasks, t)\n\t\t}\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"task\": getTaskInfos(tasks),\n\t})\n}\n\nfunc ArchiveDown(c *gin.Context) {\n\tarchiveRawPath := c.Request.Context().Value(conf.PathKey).(string)\n\tinnerPath := utils.FixAndCleanPath(c.Query(\"inner\"))\n\tpassword := c.Query(\"pass\")\n\tfilename := stdpath.Base(innerPath)\n\tstorage, err := fs.GetStorage(archiveRawPath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorPage(c, err, 500)\n\t\treturn\n\t}\n\tif common.ShouldProxy(storage, filename) {\n\t\tArchiveProxy(c)\n\t\treturn\n\t} else {\n\t\tlink, _, err := fs.ArchiveDriverExtract(c.Request.Context(), archiveRawPath, model.ArchiveInnerArgs{\n\t\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\t\tIP:       c.ClientIP(),\n\t\t\t\t\tHeader:   c.Request.Header,\n\t\t\t\t\tType:     c.Query(\"type\"),\n\t\t\t\t\tRedirect: true,\n\t\t\t\t},\n\t\t\t\tPassword: password,\n\t\t\t},\n\t\t\tInnerPath: innerPath,\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tredirect(c, link)\n\t}\n}\n\nfunc ArchiveProxy(c *gin.Context) {\n\tarchiveRawPath := c.Request.Context().Value(conf.PathKey).(string)\n\tinnerPath := utils.FixAndCleanPath(c.Query(\"inner\"))\n\tpassword := c.Query(\"pass\")\n\tfilename := stdpath.Base(innerPath)\n\tstorage, err := fs.GetStorage(archiveRawPath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorPage(c, err, 500)\n\t\treturn\n\t}\n\tif canProxy(storage, filename) {\n\t\t// TODO: Support external download proxy URL\n\t\tlink, file, err := fs.ArchiveDriverExtract(c.Request.Context(), archiveRawPath, model.ArchiveInnerArgs{\n\t\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\t\tHeader: c.Request.Header,\n\t\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t\t},\n\t\t\t\tPassword: password,\n\t\t\t},\n\t\t\tInnerPath: innerPath,\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tproxy(c, link, file, storage.GetStorage().ProxyRange)\n\t} else {\n\t\tcommon.ErrorPage(c, errors.New(\"proxy not allowed\"), 403)\n\t\treturn\n\t}\n}\n\nfunc proxyInternalExtract(c *gin.Context, rc io.ReadCloser, size int64, fileName string) {\n\tdefer func() {\n\t\tif err := rc.Close(); err != nil {\n\t\t\tlog.Errorf(\"failed to close file streamer, %v\", err)\n\t\t}\n\t}()\n\theaders := map[string]string{\n\t\t\"Referrer-Policy\": \"no-referrer\",\n\t\t\"Cache-Control\":   \"max-age=0, no-cache, no-store, must-revalidate\",\n\t}\n\theaders[\"Content-Disposition\"] = utils.GenerateContentDisposition(fileName)\n\tcontentType := c.Request.Header.Get(\"Content-Type\")\n\tif contentType == \"\" {\n\t\tcontentType = utils.GetMimeType(fileName)\n\t}\n\tc.DataFromReader(200, size, contentType, rc, headers)\n}\n\nfunc ArchiveInternalExtract(c *gin.Context) {\n\tarchiveRawPath := c.Request.Context().Value(conf.PathKey).(string)\n\tinnerPath := utils.FixAndCleanPath(c.Query(\"inner\"))\n\tpassword := c.Query(\"pass\")\n\trc, size, err := fs.ArchiveInternalExtract(c.Request.Context(), archiveRawPath, model.ArchiveInnerArgs{\n\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\tHeader: c.Request.Header,\n\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t},\n\t\t\tPassword: password,\n\t\t},\n\t\tInnerPath: innerPath,\n\t})\n\tif err != nil {\n\t\tcommon.ErrorPage(c, err, 500)\n\t\treturn\n\t}\n\tfileName := stdpath.Base(innerPath)\n\tproxyInternalExtract(c, rc, size, fileName)\n}\n\nfunc ArchiveExtensions(c *gin.Context) {\n\tvar ext []string\n\tfor key := range tool.Tools {\n\t\text = append(ext, key)\n\t}\n\tcommon.SuccessResp(c, ext)\n}\n"
  },
  {
    "path": "server/handles/auth.go",
    "content": "package handles\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"image/png\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\ntype LoginReq struct {\n\tUsername string `json:\"username\" binding:\"required\"`\n\tPassword string `json:\"password\"`\n\tOtpCode  string `json:\"otp_code\"`\n}\n\n// Login Deprecated\nfunc Login(c *gin.Context) {\n\tvar req LoginReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Password = model.StaticHash(req.Password)\n\tloginHash(c, &req)\n}\n\n// LoginHash login with password hashed by sha256\nfunc LoginHash(c *gin.Context) {\n\tvar req LoginReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tloginHash(c, &req)\n}\n\nfunc loginHash(c *gin.Context, req *LoginReq) {\n\t// check count of login\n\tip := c.ClientIP()\n\tcount, ok := model.LoginCache.Get(ip)\n\tif ok && count >= model.DefaultMaxAuthRetries {\n\t\tcommon.ErrorStrResp(c, model.TooManyAttempts, 429)\n\t\tmodel.LoginCache.Expire(ip, model.DefaultLockDuration)\n\t\treturn\n\t}\n\t// check username\n\tuser, err := op.GetUserByName(req.Username)\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, model.InvalidUsernameOrPassword, 401)\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\treturn\n\t}\n\t// validate password hash\n\tif err := user.ValidatePwdStaticHash(req.Password); err != nil {\n\t\tcommon.ErrorStrResp(c, model.InvalidUsernameOrPassword, 401)\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\treturn\n\t}\n\t// check 2FA\n\tif user.OtpSecret != \"\" {\n\t\tif !totp.Validate(req.OtpCode, user.OtpSecret) {\n\t\t\t// 402 - need opt\n\t\t\tcommon.ErrorStrResp(c, model.Invalid2FACode, 402)\n\t\t\tmodel.LoginCache.Set(ip, count+1)\n\t\t\treturn\n\t\t}\n\t}\n\t// generate token\n\ttoken, err := common.GenerateToken(user)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\"token\": token})\n\tmodel.LoginCache.Del(ip)\n}\n\ntype UserResp struct {\n\tmodel.User\n\tOtp bool `json:\"otp\"`\n}\n\n// CurrentUser get current user by token\n// if token is empty, return guest user\nfunc CurrentUser(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tuserResp := UserResp{\n\t\tUser: *user,\n\t}\n\tuserResp.Password = \"\"\n\tif userResp.OtpSecret != \"\" {\n\t\tuserResp.Otp = true\n\t}\n\tcommon.SuccessResp(c, userResp)\n}\n\nfunc UpdateCurrent(c *gin.Context) {\n\tvar req model.User\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, model.GuestCannotUpdateProfile, 403)\n\t\treturn\n\t}\n\tuser.Username = req.Username\n\tif req.Password != \"\" {\n\t\tuser.SetPassword(req.Password)\n\t}\n\tuser.SsoID = req.SsoID\n\tif err := op.UpdateUser(user); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc Generate2FA(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, model.GuestCannotGenerate2FA, 403)\n\t\treturn\n\t}\n\tkey, err := totp.Generate(totp.GenerateOpts{\n\t\tIssuer:      \"OpenList\",\n\t\tAccountName: user.Username,\n\t})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\timg, err := key.Image(400, 400)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t// to base64\n\tvar buf bytes.Buffer\n\tpng.Encode(&buf, img)\n\tb64 := base64.StdEncoding.EncodeToString(buf.Bytes())\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"qr\":     \"data:image/png;base64,\" + b64,\n\t\t\"secret\": key.Secret(),\n\t})\n}\n\ntype Verify2FAReq struct {\n\tCode   string `json:\"code\" binding:\"required\"`\n\tSecret string `json:\"secret\" binding:\"required\"`\n}\n\nfunc Verify2FA(c *gin.Context) {\n\tvar req Verify2FAReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, model.GuestCannotGenerate2FA, 403)\n\t\treturn\n\t}\n\tif !totp.Validate(req.Code, req.Secret) {\n\t\tcommon.ErrorStrResp(c, model.Invalid2FACode, 400)\n\t\treturn\n\t}\n\tuser.OtpSecret = req.Secret\n\tif err := op.UpdateUser(user); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc LogOut(c *gin.Context) {\n\terr := common.InvalidateToken(c.GetHeader(\"Authorization\"))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n"
  },
  {
    "path": "server/handles/const.go",
    "content": "package handles\n\nconst (\n\tCANCEL    = \"cancel\"\n\tOVERWRITE = \"overwrite\"\n\tSKIP      = \"skip\"\n)\n"
  },
  {
    "path": "server/handles/direct_upload.go",
    "content": "package handles\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype FsGetDirectUploadInfoReq struct {\n\tPath     string `json:\"path\" form:\"path\"`\n\tFileName string `json:\"file_name\" form:\"file_name\"`\n\tFileSize int64  `json:\"file_size\" form:\"file_size\"`\n\tTool     string `json:\"tool\" form:\"tool\"`\n}\n\n// FsGetDirectUploadInfo returns the direct upload info if supported by the driver\n// If the driver does not support direct upload, returns null for upload_info\nfunc FsGetDirectUploadInfo(c *gin.Context) {\n\tvar req FsGetDirectUploadInfoReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\t// Decode path\n\tpath, err := url.PathUnescape(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\t// Get user and join path\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tpath, err = user.JoinPath(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\toverwrite := c.GetHeader(\"Overwrite\") != \"false\"\n\tif !overwrite {\n\t\tif res, _ := fs.Get(c.Request.Context(), path, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\tcommon.ErrorStrResp(c, \"file exists\", 403)\n\t\t\treturn\n\t\t}\n\t}\n\tdirectUploadInfo, err := fs.GetDirectUploadInfo(c, req.Tool, path, req.FileName, req.FileSize)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, directUploadInfo)\n}\n"
  },
  {
    "path": "server/handles/down.go",
    "content": "package handles\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/microcosm-cc/bluemonday\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/yuin/goldmark\"\n)\n\nfunc Down(c *gin.Context) {\n\trawPath := c.Request.Context().Value(conf.PathKey).(string)\n\tfilename := stdpath.Base(rawPath)\n\tstorage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorPage(c, err, 500)\n\t\treturn\n\t}\n\tif common.ShouldProxy(storage, filename) {\n\t\tProxy(c)\n\t\treturn\n\t} else {\n\t\tlink, _, err := fs.Link(c.Request.Context(), rawPath, model.LinkArgs{\n\t\t\tIP:       c.ClientIP(),\n\t\t\tHeader:   c.Request.Header,\n\t\t\tType:     c.Query(\"type\"),\n\t\t\tRedirect: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tredirect(c, link)\n\t}\n}\n\nfunc Proxy(c *gin.Context) {\n\trawPath := c.Request.Context().Value(conf.PathKey).(string)\n\tfilename := stdpath.Base(rawPath)\n\tstorage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorPage(c, err, 500)\n\t\treturn\n\t}\n\tif canProxy(storage, filename) {\n\t\tif _, ok := c.GetQuery(\"d\"); !ok {\n\t\t\tif url := common.GenerateDownProxyURL(storage.GetStorage(), rawPath); url != \"\" {\n\t\t\t\tc.Redirect(302, url)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tlink, file, err := fs.Link(c.Request.Context(), rawPath, model.LinkArgs{\n\t\t\tHeader: c.Request.Header,\n\t\t\tType:   c.Query(\"type\"),\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tproxy(c, link, file, storage.GetStorage().ProxyRange)\n\t} else {\n\t\tcommon.ErrorPage(c, errors.New(\"proxy not allowed\"), 403)\n\t\treturn\n\t}\n}\n\nfunc redirect(c *gin.Context, link *model.Link) {\n\tdefer link.Close()\n\tvar err error\n\tc.Header(\"Referrer-Policy\", \"no-referrer\")\n\tc.Header(\"Cache-Control\", \"max-age=0, no-cache, no-store, must-revalidate\")\n\tif setting.GetBool(conf.ForwardDirectLinkParams) {\n\t\tquery := c.Request.URL.Query()\n\t\tfor _, v := range conf.SlicesMap[conf.IgnoreDirectLinkParams] {\n\t\t\tquery.Del(v)\n\t\t}\n\t\tlink.URL, err = utils.InjectQuery(link.URL, query)\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tc.Redirect(302, link.URL)\n}\n\nfunc proxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange bool) {\n\tdefer link.Close()\n\tvar err error\n\tif link.URL != \"\" && setting.GetBool(conf.ForwardDirectLinkParams) {\n\t\tquery := c.Request.URL.Query()\n\t\tfor _, v := range conf.SlicesMap[conf.IgnoreDirectLinkParams] {\n\t\t\tquery.Del(v)\n\t\t}\n\t\tlink.URL, err = utils.InjectQuery(link.URL, query)\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tif proxyRange {\n\t\tlink = common.ProxyRange(c, link, file.GetSize())\n\t}\n\tWriter := &common.WrittenResponseWriter{ResponseWriter: c.Writer}\n\traw, _ := strconv.ParseBool(c.DefaultQuery(\"raw\", \"false\"))\n\tif utils.Ext(file.GetName()) == \"md\" && setting.GetBool(conf.FilterReadMeScripts) && !raw {\n\t\tbuf := bytes.NewBuffer(make([]byte, 0, file.GetSize()))\n\t\tw := &common.InterceptResponseWriter{ResponseWriter: Writer, Writer: buf}\n\t\terr = common.Proxy(w, c.Request, link, file)\n\t\tif err == nil && buf.Len() > 0 {\n\t\t\tif c.Writer.Status() < 200 || c.Writer.Status() > 300 {\n\t\t\t\tc.Writer.Write(buf.Bytes())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar html bytes.Buffer\n\t\t\tif err = goldmark.Convert(buf.Bytes(), &html); err != nil {\n\t\t\t\terr = fmt.Errorf(\"markdown conversion failed: %w\", err)\n\t\t\t} else {\n\t\t\t\tbuf.Reset()\n\t\t\t\terr = bluemonday.UGCPolicy().SanitizeReaderToWriter(&html, buf)\n\t\t\t\tif err == nil {\n\t\t\t\t\tWriter.Header().Set(\"Content-Length\", strconv.FormatInt(int64(buf.Len()), 10))\n\t\t\t\t\tWriter.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\t\t\t\t_, err = utils.CopyWithBuffer(Writer, buf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\terr = common.Proxy(Writer, c.Request, link, file)\n\t}\n\tif err == nil {\n\t\treturn\n\t}\n\tif Writer.IsWritten() {\n\t\tlog.Errorf(\"%s %s local proxy error: %+v\", c.Request.Method, c.Request.URL.Path, err)\n\t} else {\n\t\tif statusCode, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {\n\t\t\tcommon.ErrorPage(c, err, int(statusCode), true)\n\t\t} else {\n\t\t\tcommon.ErrorPage(c, err, 500, true)\n\t\t}\n\t}\n}\n\n// TODO need optimize\n// when can be proxy?\n// 1. text file\n// 2. config.MustProxy()\n// 3. storage.WebProxy\n// 4. proxy_types\n// solution: text_file + shouldProxy()\nfunc canProxy(storage driver.Driver, filename string) bool {\n\tif storage.Config().MustProxy() || storage.GetStorage().WebProxy || storage.GetStorage().WebdavProxyURL() {\n\t\treturn true\n\t}\n\tif utils.SliceContains(conf.SlicesMap[conf.ProxyTypes], utils.Ext(filename)) {\n\t\treturn true\n\t}\n\tif utils.SliceContains(conf.SlicesMap[conf.TextTypes], utils.Ext(filename)) {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/handles/driver.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc ListDriverInfo(c *gin.Context) {\n\tcommon.SuccessResp(c, op.GetDriverInfoMap())\n}\n\nfunc ListDriverNames(c *gin.Context) {\n\tcommon.SuccessResp(c, op.GetDriverNames())\n}\n\nfunc GetDriverInfo(c *gin.Context) {\n\tdriverName := c.Query(\"driver\")\n\tinfoMap := op.GetDriverInfoMap()\n\titems, ok := infoMap[driverName]\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"driver [%s] not found\", driverName), 404)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, items)\n}\n"
  },
  {
    "path": "server/handles/fsbatch.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"slices\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\ntype RecursiveMoveReq struct {\n\tSrcDir         string `json:\"src_dir\"`\n\tDstDir         string `json:\"dst_dir\"`\n\tConflictPolicy string `json:\"conflict_policy\"`\n}\n\nfunc FsRecursiveMove(c *gin.Context) {\n\tvar req RecursiveMoveReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanMove() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tdstDir, err := user.JoinPath(req.DstDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tmeta, err := op.GetNearestMeta(srcDir)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\n\trootFiles, err := fs.List(c.Request.Context(), srcDir, &fs.ListArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\n\tvar existingFileNames []string\n\tif req.ConflictPolicy != OVERWRITE {\n\t\tdstFiles, err := fs.List(c.Request.Context(), dstDir, &fs.ListArgs{})\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\texistingFileNames = make([]string, 0, len(dstFiles))\n\t\tfor _, dstFile := range dstFiles {\n\t\t\texistingFileNames = append(existingFileNames, dstFile.GetName())\n\t\t}\n\t}\n\n\t// record the file path\n\tfilePathMap := make(map[model.Obj]string)\n\tmovingFiles := generic.NewQueue[model.Obj]()\n\tmovingFileNames := make([]string, 0, len(rootFiles))\n\tfor _, file := range rootFiles {\n\t\tmovingFiles.Push(file)\n\t\tfilePathMap[file] = srcDir\n\t}\n\n\tfor !movingFiles.IsEmpty() {\n\n\t\tmovingFile := movingFiles.Pop()\n\t\tmovingFilePath := filePathMap[movingFile]\n\t\tmovingFileName := fmt.Sprintf(\"%s/%s\", movingFilePath, movingFile.GetName())\n\t\tif movingFile.IsDir() {\n\t\t\t// directory, recursive move\n\t\t\tsubFilePath := movingFileName\n\t\t\tsubFiles, err := fs.List(c.Request.Context(), movingFileName, &fs.ListArgs{Refresh: true})\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor _, subFile := range subFiles {\n\t\t\t\tmovingFiles.Push(subFile)\n\t\t\t\tfilePathMap[subFile] = subFilePath\n\t\t\t}\n\t\t} else {\n\t\t\tif movingFilePath == dstDir {\n\t\t\t\t// same directory, don't move\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif slices.Contains(existingFileNames, movingFile.GetName()) {\n\t\t\t\tif req.ConflictPolicy == CANCEL {\n\t\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", movingFile.GetName()), 403)\n\t\t\t\t\treturn\n\t\t\t\t} else if req.ConflictPolicy == SKIP {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else if req.ConflictPolicy != OVERWRITE {\n\t\t\t\texistingFileNames = append(existingFileNames, movingFile.GetName())\n\t\t\t}\n\t\t\tmovingFileNames = append(movingFileNames, movingFileName)\n\n\t\t}\n\n\t}\n\n\tvar count = 0\n\tfor i, fileName := range movingFileNames {\n\t\t// move\n\t\t_, err := fs.Move(c.Request.Context(), fileName, dstDir, len(movingFileNames) > i+1)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tcount++\n\t}\n\n\tcommon.SuccessWithMsgResp(c, fmt.Sprintf(\"Successfully moved %d %s\", count, common.Pluralize(count, \"file\", \"files\")))\n}\n\ntype BatchRenameReq struct {\n\tSrcDir        string `json:\"src_dir\"`\n\tRenameObjects []struct {\n\t\tSrcName string `json:\"src_name\"`\n\t\tNewName string `json:\"new_name\"`\n\t} `json:\"rename_objects\"`\n}\n\nfunc FsBatchRename(c *gin.Context) {\n\tvar req BatchRenameReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanRename() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\n\treqPath, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tfor _, renameObject := range req.RenameObjects {\n\t\tif renameObject.SrcName == \"\" || renameObject.NewName == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\terr = checkRelativePath(renameObject.NewName)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t\tfilePath := fmt.Sprintf(\"%s/%s\", reqPath, renameObject.SrcName)\n\t\tif err := fs.Rename(c.Request.Context(), filePath, renameObject.NewName); err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.SuccessResp(c)\n}\n\ntype RegexRenameReq struct {\n\tSrcDir       string `json:\"src_dir\"`\n\tSrcNameRegex string `json:\"src_name_regex\"`\n\tNewNameRegex string `json:\"new_name_regex\"`\n}\n\nfunc FsRegexRename(c *gin.Context) {\n\tvar req RegexRenameReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanRename() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\n\treqPath, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\n\tsrcRegexp, err := regexp.Compile(req.SrcNameRegex)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\n\tfiles, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\n\tfor _, file := range files {\n\t\tif srcRegexp.MatchString(file.GetName()) {\n\t\t\tnewFileName := srcRegexp.ReplaceAllString(file.GetName(), req.NewNameRegex)\n\t\t\terr := checkRelativePath(newFileName)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfilePath := fmt.Sprintf(\"%s/%s\", reqPath, file.GetName())\n\t\t\tif err := fs.Rename(c.Request.Context(), filePath, newFileName); err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t}\n\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/fsmanage.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/generic\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype MkdirOrLinkReq struct {\n\tPath string `json:\"path\" form:\"path\"`\n}\n\nfunc FsMkdir(c *gin.Context) {\n\tvar req MkdirOrLinkReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !user.CanWrite() {\n\t\tmeta, err := op.GetNearestMeta(stdpath.Dir(reqPath))\n\t\tif err != nil {\n\t\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif !common.CanWrite(meta, reqPath) {\n\t\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\t\treturn\n\t\t}\n\t}\n\tif err := fs.MakeDir(c.Request.Context(), reqPath); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\ntype MoveCopyReq struct {\n\tSrcDir       string   `json:\"src_dir\"`\n\tDstDir       string   `json:\"dst_dir\"`\n\tNames        []string `json:\"names\"`\n\tOverwrite    bool     `json:\"overwrite\"`\n\tSkipExisting bool     `json:\"skip_existing\"`\n\tMerge        bool     `json:\"merge\"`\n}\n\nfunc FsMove(c *gin.Context) {\n\tvar req MoveCopyReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Names) == 0 {\n\t\tcommon.ErrorStrResp(c, \"Empty file names\", 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanMove() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tdstDir, err := user.JoinPath(req.DstDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tvalidPaths := make([]string, 0, len(req.Names))\n\tfor _, name := range req.Names {\n\t\t// ensure req.Names is not a relative path\n\t\tsrcPath := stdpath.Join(req.SrcDir, name)\n\t\tsrcPath, err = user.JoinPath(srcPath)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t\tif !req.Overwrite {\n\t\t\tbase := stdpath.Base(srcPath)\n\t\t\tif base == \".\" || base == \"/\" {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"invalid file name [%s]\", name), 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, base), &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t\tif !req.SkipExisting {\n\t\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", name), 403)\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvalidPaths = append(validPaths, srcPath)\n\t}\n\n\t// Create all tasks immediately without any synchronous validation\n\t// All validation will be done asynchronously in the background\n\tvar addedTasks []task.TaskExtensionInfo\n\tfor i, p := range validPaths {\n\t\tt, err := fs.Move(c.Request.Context(), p, dstDir, len(validPaths) > i+1)\n\t\tif t != nil {\n\t\t\taddedTasks = append(addedTasks, t)\n\t\t}\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Return immediately with task information\n\tif len(addedTasks) > 0 {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"message\": fmt.Sprintf(\"Successfully created %d move task(s)\", len(addedTasks)),\n\t\t\t\"tasks\":   getTaskInfos(addedTasks),\n\t\t})\n\t} else {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"message\": \"Move operations completed immediately\",\n\t\t})\n\t}\n}\n\nfunc FsCopy(c *gin.Context) {\n\tvar req MoveCopyReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Names) == 0 {\n\t\tcommon.ErrorStrResp(c, \"Empty file names\", 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanCopy() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tdstDir, err := user.JoinPath(req.DstDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tvalidPaths := make([]string, 0, len(req.Names))\n\tfor _, name := range req.Names {\n\t\t// ensure req.Names is not a relative path\n\t\tsrcPath := stdpath.Join(req.SrcDir, name)\n\t\tsrcPath, err = user.JoinPath(srcPath)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t\tif !req.Overwrite {\n\t\t\tbase := stdpath.Base(srcPath)\n\t\t\tif base == \".\" || base == \"/\" {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"invalid file name [%s]\", name), 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, base), &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t\tif !req.SkipExisting && !req.Merge {\n\t\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", name), 403)\n\t\t\t\t\treturn\n\t\t\t\t} else if !req.Merge || !res.IsDir() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvalidPaths = append(validPaths, srcPath)\n\t}\n\n\t// Create all tasks immediately without any synchronous validation\n\t// All validation will be done asynchronously in the background\n\tvar addedTasks []task.TaskExtensionInfo\n\tfor i, p := range validPaths {\n\t\tvar t task.TaskExtensionInfo\n\t\tif req.Merge {\n\t\t\tt, err = fs.Merge(c.Request.Context(), p, dstDir, len(validPaths) > i+1)\n\t\t} else {\n\t\t\tt, err = fs.Copy(c.Request.Context(), p, dstDir, len(validPaths) > i+1)\n\t\t}\n\t\tif t != nil {\n\t\t\taddedTasks = append(addedTasks, t)\n\t\t}\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Return immediately with task information\n\tif len(addedTasks) > 0 {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"message\": fmt.Sprintf(\"Successfully created %d copy task(s)\", len(addedTasks)),\n\t\t\t\"tasks\":   getTaskInfos(addedTasks),\n\t\t})\n\t} else {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"message\": \"Copy operations completed immediately\",\n\t\t})\n\t}\n}\n\ntype RenameReq struct {\n\tPath      string `json:\"path\"`\n\tName      string `json:\"name\"`\n\tOverwrite bool   `json:\"overwrite\"`\n}\n\nfunc FsRename(c *gin.Context) {\n\tvar req RenameReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanRename() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\treqPath, err := user.JoinPath(req.Path)\n\tif err == nil {\n\t\terr = checkRelativePath(req.Name)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !req.Overwrite {\n\t\tdstPath := stdpath.Join(stdpath.Dir(reqPath), req.Name)\n\t\tif dstPath != reqPath {\n\t\t\tif res, _ := fs.Get(c.Request.Context(), dstPath, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", req.Name), 403)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tif err := fs.Rename(c.Request.Context(), reqPath, req.Name); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc checkRelativePath(path string) error {\n\tif strings.ContainsAny(path, \"/\\\\\") || path == \"\" || path == \".\" || path == \"..\" {\n\t\treturn errs.RelativePath\n\t}\n\treturn nil\n}\n\ntype RemoveReq struct {\n\tDir   string   `json:\"dir\"`\n\tNames []string `json:\"names\"`\n}\n\nfunc FsRemove(c *gin.Context) {\n\tvar req RemoveReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Names) == 0 {\n\t\tcommon.ErrorStrResp(c, \"Empty file names\", 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanRemove() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tfor i, name := range req.Names {\n\t\tif strings.TrimSpace(utils.FixAndCleanPath(name)) == \"/\" {\n\t\t\tlog.Warnf(\"FsRemove: invalid item skipped: %s (parent directory: %s)\\n\", name, req.Dir)\n\t\t\treq.Names[i] = \"\"\n\t\t\tcontinue\n\t\t}\n\t\t// ensure req.Names is not a relative path\n\t\tvar err error\n\t\treq.Names[i], err = user.JoinPath(stdpath.Join(req.Dir, name))\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t}\n\tfor _, path := range req.Names {\n\t\tif path == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\terr := fs.Remove(c.Request.Context(), path)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\t//fs.ClearCache(req.Dir)\n\tcommon.SuccessResp(c)\n}\n\ntype RemoveEmptyDirectoryReq struct {\n\tSrcDir string `json:\"src_dir\"`\n}\n\nfunc FsRemoveEmptyDirectory(c *gin.Context) {\n\tvar req RemoveEmptyDirectoryReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanRemove() {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\n\tmeta, err := op.GetNearestMeta(srcDir)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\n\trootFiles, err := fs.List(c.Request.Context(), srcDir, &fs.ListArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\n\t// record the file path\n\tfilePathMap := make(map[model.Obj]string)\n\t// record the parent file\n\tfileParentMap := make(map[model.Obj]model.Obj)\n\t// removing files\n\tremovingFiles := generic.NewQueue[model.Obj]()\n\t// removed files\n\tremovedFiles := make(map[string]bool)\n\tfor _, file := range rootFiles {\n\t\tif !file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tremovingFiles.Push(file)\n\t\tfilePathMap[file] = srcDir\n\t}\n\n\tfor !removingFiles.IsEmpty() {\n\n\t\tremovingFile := removingFiles.Pop()\n\t\tremovingFilePath := fmt.Sprintf(\"%s/%s\", filePathMap[removingFile], removingFile.GetName())\n\n\t\tif removedFiles[removingFilePath] {\n\t\t\tcontinue\n\t\t}\n\n\t\tsubFiles, err := fs.List(c.Request.Context(), removingFilePath, &fs.ListArgs{Refresh: true})\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\n\t\tif len(subFiles) == 0 {\n\t\t\t// remove empty directory\n\t\t\terr = fs.Remove(c.Request.Context(), removingFilePath)\n\t\t\tremovedFiles[removingFilePath] = true\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// recheck parent folder\n\t\t\tparentFile, exist := fileParentMap[removingFile]\n\t\t\tif exist {\n\t\t\t\tremovingFiles.Push(parentFile)\n\t\t\t}\n\n\t\t} else {\n\t\t\t// recursive remove\n\t\t\tfor _, subFile := range subFiles {\n\t\t\t\tif !subFile.IsDir() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tremovingFiles.Push(subFile)\n\t\t\t\tfilePathMap[subFile] = removingFilePath\n\t\t\t\tfileParentMap[subFile] = removingFile\n\t\t\t}\n\t\t}\n\n\t}\n\n\tcommon.SuccessResp(c)\n}\n\n// Link return real link, just for proxy program, it may contain cookie, so just allowed for admin\nfunc Link(c *gin.Context) {\n\tvar req MkdirOrLinkReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\t//user := c.Request.Context().Value(conf.UserKey).(*model.User)\n\t//rawPath := stdpath.Join(user.BasePath, req.Path)\n\t// why need not join base_path? because it's always the full path\n\trawPath := req.Path\n\tstorage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif storage.Config().NoLinkURL {\n\t\tcommon.SuccessResp(c, model.Link{\n\t\t\tURL: fmt.Sprintf(\"%s/p%s?d&sign=%s\",\n\t\t\t\tcommon.GetApiUrl(c),\n\t\t\t\tutils.EncodePath(rawPath, true),\n\t\t\t\tsign.Sign(rawPath)),\n\t\t})\n\t\treturn\n\t}\n\tlink, _, err := fs.Link(c.Request.Context(), rawPath, model.LinkArgs{IP: c.ClientIP(), Header: c.Request.Header, Redirect: true})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tdefer link.Close()\n\tcommon.SuccessResp(c, link)\n}\n"
  },
  {
    "path": "server/handles/fsread.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\ntype ListReq struct {\n\tmodel.PageReq\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n\tRefresh  bool   `json:\"refresh\"`\n}\n\ntype DirReq struct {\n\tPath      string `json:\"path\" form:\"path\"`\n\tPassword  string `json:\"password\" form:\"password\"`\n\tForceRoot bool   `json:\"force_root\" form:\"force_root\"`\n}\n\ntype ObjResp struct {\n\tName         string                     `json:\"name\"`\n\tSize         int64                      `json:\"size\"`\n\tIsDir        bool                       `json:\"is_dir\"`\n\tModified     time.Time                  `json:\"modified\"`\n\tCreated      time.Time                  `json:\"created\"`\n\tSign         string                     `json:\"sign\"`\n\tThumb        string                     `json:\"thumb\"`\n\tType         int                        `json:\"type\"`\n\tHashInfoStr  string                     `json:\"hashinfo\"`\n\tHashInfo     map[*utils.HashType]string `json:\"hash_info\"`\n\tMountDetails *model.StorageDetails      `json:\"mount_details,omitempty\"`\n}\n\ntype FsListResp struct {\n\tContent           []ObjResp `json:\"content\"`\n\tTotal             int64     `json:\"total\"`\n\tReadme            string    `json:\"readme\"`\n\tHeader            string    `json:\"header\"`\n\tWrite             bool      `json:\"write\"`\n\tProvider          string    `json:\"provider\"`\n\tDirectUploadTools []string  `json:\"direct_upload_tools,omitempty\"`\n}\n\nfunc FsListSplit(c *gin.Context) {\n\tvar req ListReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tif strings.HasPrefix(req.Path, \"/@s\") {\n\t\treq.Path = strings.TrimPrefix(req.Path, \"/@s\")\n\t\tSharingList(c, &req)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() && user.Disabled {\n\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\treturn\n\t}\n\tFsList(c, &req, user)\n}\n\nfunc FsList(c *gin.Context, req *ListReq, user *model.User) {\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tif !user.CanWrite() && !common.CanWrite(meta, reqPath) && req.Refresh {\n\t\tcommon.ErrorStrResp(c, \"Refresh without permission\", 403)\n\t\treturn\n\t}\n\tobjs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{\n\t\tRefresh:            req.Refresh,\n\t\tWithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),\n\t})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\ttotal, objs := pagination(objs, &req.PageReq)\n\tprovider := \"unknown\"\n\tvar directUploadTools []string\n\tif user.CanWrite() {\n\t\tif storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}); err == nil {\n\t\t\tdirectUploadTools = op.GetDirectUploadTools(storage)\n\t\t}\n\t}\n\tcommon.SuccessResp(c, FsListResp{\n\t\tContent:           toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)),\n\t\tTotal:             int64(total),\n\t\tReadme:            getReadme(meta, reqPath),\n\t\tHeader:            getHeader(meta, reqPath),\n\t\tWrite:             user.CanWrite() || common.CanWrite(meta, reqPath),\n\t\tProvider:          provider,\n\t\tDirectUploadTools: directUploadTools,\n\t})\n}\n\nfunc FsDirs(c *gin.Context) {\n\tvar req DirReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\treqPath := req.Path\n\tif req.ForceRoot {\n\t\tif !user.IsAdmin() {\n\t\t\tcommon.ErrorStrResp(c, \"Permission denied\", 403)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\ttmp, err := user.JoinPath(req.Path)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t\treturn\n\t\t}\n\t\treqPath = tmp\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tobjs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tdirs := filterDirs(objs)\n\tcommon.SuccessResp(c, dirs)\n}\n\ntype DirResp struct {\n\tName     string    `json:\"name\"`\n\tModified time.Time `json:\"modified\"`\n}\n\nfunc filterDirs(objs []model.Obj) []DirResp {\n\tvar dirs []DirResp\n\tfor _, obj := range objs {\n\t\tif obj.IsDir() {\n\t\t\tdirs = append(dirs, DirResp{\n\t\t\t\tName:     obj.GetName(),\n\t\t\t\tModified: obj.ModTime(),\n\t\t\t})\n\t\t}\n\t}\n\treturn dirs\n}\n\nfunc getReadme(meta *model.Meta, path string) string {\n\tif meta != nil && (utils.PathEqual(meta.Path, path) || meta.RSub) {\n\t\treturn meta.Readme\n\t}\n\treturn \"\"\n}\n\nfunc getHeader(meta *model.Meta, path string) string {\n\tif meta != nil && (utils.PathEqual(meta.Path, path) || meta.HeaderSub) {\n\t\treturn meta.Header\n\t}\n\treturn \"\"\n}\n\nfunc isEncrypt(meta *model.Meta, path string) bool {\n\tif common.IsStorageSignEnabled(path) {\n\t\treturn true\n\t}\n\tif meta == nil || meta.Password == \"\" {\n\t\treturn false\n\t}\n\tif !utils.PathEqual(meta.Path, path) && !meta.PSub {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {\n\tpageIndex, pageSize := req.Page, req.PerPage\n\ttotal := len(objs)\n\tstart := (pageIndex - 1) * pageSize\n\tif start > total {\n\t\treturn total, []model.Obj{}\n\t}\n\tend := start + pageSize\n\tif end > total {\n\t\tend = total\n\t}\n\treturn total, objs[start:end]\n}\n\nfunc toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {\n\tvar resp []ObjResp\n\tfor _, obj := range objs {\n\t\tthumb, _ := model.GetThumb(obj)\n\t\tmountDetails, _ := model.GetStorageDetails(obj)\n\t\tresp = append(resp, ObjResp{\n\t\t\tName:         obj.GetName(),\n\t\t\tSize:         obj.GetSize(),\n\t\t\tIsDir:        obj.IsDir(),\n\t\t\tModified:     obj.ModTime(),\n\t\t\tCreated:      obj.CreateTime(),\n\t\t\tHashInfoStr:  obj.GetHash().String(),\n\t\t\tHashInfo:     obj.GetHash().Export(),\n\t\t\tSign:         common.Sign(obj, parent, encrypt),\n\t\t\tThumb:        thumb,\n\t\t\tType:         utils.GetObjType(obj.GetName(), obj.IsDir()),\n\t\t\tMountDetails: mountDetails,\n\t\t})\n\t}\n\treturn resp\n}\n\ntype FsGetReq struct {\n\tPath     string `json:\"path\" form:\"path\"`\n\tPassword string `json:\"password\" form:\"password\"`\n}\n\ntype FsGetResp struct {\n\tObjResp\n\tRawURL   string    `json:\"raw_url\"`\n\tReadme   string    `json:\"readme\"`\n\tHeader   string    `json:\"header\"`\n\tProvider string    `json:\"provider\"`\n\tRelated  []ObjResp `json:\"related\"`\n}\n\nfunc FsGetSplit(c *gin.Context) {\n\tvar req FsGetReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif strings.HasPrefix(req.Path, \"/@s\") {\n\t\treq.Path = strings.TrimPrefix(req.Path, \"/@s\")\n\t\tSharingGet(c, &req)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() && user.Disabled {\n\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\treturn\n\t}\n\tFsGet(c, &req, user)\n}\n\nfunc FsGet(c *gin.Context, req *FsGetReq, user *model.User) {\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(reqPath)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, reqPath, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tobj, err := fs.Get(c.Request.Context(), reqPath, &fs.GetArgs{\n\t\tWithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),\n\t})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tvar rawURL string\n\n\tstorage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})\n\tprovider, ok := model.GetProvider(obj)\n\tif !ok && err == nil {\n\t\tprovider = storage.Config().Name\n\t}\n\tif !obj.IsDir() {\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().MustProxy() || storage.GetStorage().WebProxy {\n\t\t\trawURL = common.GenerateDownProxyURL(storage.GetStorage(), reqPath)\n\t\t\tif rawURL == \"\" {\n\t\t\t\tquery := \"\"\n\t\t\t\tif isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {\n\t\t\t\t\tquery = \"?sign=\" + sign.Sign(reqPath)\n\t\t\t\t}\n\t\t\t\trawURL = fmt.Sprintf(\"%s/p%s%s\",\n\t\t\t\t\tcommon.GetApiUrl(c),\n\t\t\t\t\tutils.EncodePath(reqPath, true),\n\t\t\t\t\tquery)\n\t\t\t}\n\t\t} else {\n\t\t\t// file have raw url\n\t\t\tif url, ok := model.GetUrl(obj); ok {\n\t\t\t\trawURL = url\n\t\t\t} else {\n\t\t\t\t// if storage is not proxy, use raw url by fs.Link\n\t\t\t\tlink, _, err := fs.Link(c.Request.Context(), reqPath, model.LinkArgs{\n\t\t\t\t\tIP:       c.ClientIP(),\n\t\t\t\t\tHeader:   c.Request.Header,\n\t\t\t\t\tRedirect: true,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tdefer link.Close()\n\t\t\t\trawURL = link.URL\n\t\t\t}\n\t\t}\n\t}\n\tvar related []model.Obj\n\tparentPath := stdpath.Dir(reqPath)\n\tsameLevelFiles, err := fs.List(c.Request.Context(), parentPath, &fs.ListArgs{})\n\tif err == nil {\n\t\trelated = filterRelated(sameLevelFiles, obj)\n\t}\n\tparentMeta, _ := op.GetNearestMeta(parentPath)\n\tthumb, _ := model.GetThumb(obj)\n\tmountDetails, _ := model.GetStorageDetails(obj)\n\tcommon.SuccessResp(c, FsGetResp{\n\t\tObjResp: ObjResp{\n\t\t\tName:         obj.GetName(),\n\t\t\tSize:         obj.GetSize(),\n\t\t\tIsDir:        obj.IsDir(),\n\t\t\tModified:     obj.ModTime(),\n\t\t\tCreated:      obj.CreateTime(),\n\t\t\tHashInfoStr:  obj.GetHash().String(),\n\t\t\tHashInfo:     obj.GetHash().Export(),\n\t\t\tSign:         common.Sign(obj, parentPath, isEncrypt(meta, reqPath)),\n\t\t\tType:         utils.GetFileType(obj.GetName()),\n\t\t\tThumb:        thumb,\n\t\t\tMountDetails: mountDetails,\n\t\t},\n\t\tRawURL:   rawURL,\n\t\tReadme:   getReadme(meta, reqPath),\n\t\tHeader:   getHeader(meta, reqPath),\n\t\tProvider: provider,\n\t\tRelated:  toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)),\n\t})\n}\n\nfunc filterRelated(objs []model.Obj, obj model.Obj) []model.Obj {\n\tvar related []model.Obj\n\tnameWithoutExt := strings.TrimSuffix(obj.GetName(), stdpath.Ext(obj.GetName()))\n\tfor _, o := range objs {\n\t\tif o.GetName() == obj.GetName() {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(o.GetName(), nameWithoutExt) {\n\t\t\trelated = append(related, o)\n\t\t}\n\t}\n\treturn related\n}\n\ntype FsOtherReq struct {\n\tmodel.FsOtherArgs\n\tPassword string `json:\"password\" form:\"password\"`\n}\n\nfunc FsOther(c *gin.Context) {\n\tvar req FsOtherReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tvar err error\n\treq.Path, err = user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(req.Path)\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\tif !common.CanAccess(user, meta, req.Path, req.Password) {\n\t\tcommon.ErrorStrResp(c, \"password is incorrect or you have no permission\", 403)\n\t\treturn\n\t}\n\tres, err := fs.Other(c.Request.Context(), req.FsOtherArgs)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, res)\n}\n"
  },
  {
    "path": "server/handles/fsup.go",
    "content": "package handles\n\nimport (\n\t\"io\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc getLastModified(c *gin.Context) time.Time {\n\tnow := time.Now()\n\tlastModifiedStr := c.GetHeader(\"Last-Modified\")\n\tlastModifiedMillisecond, err := strconv.ParseInt(lastModifiedStr, 10, 64)\n\tif err != nil {\n\t\treturn now\n\t}\n\tlastModified := time.UnixMilli(lastModifiedMillisecond)\n\treturn lastModified\n}\n\n// shouldIgnoreSystemFile checks if the filename should be ignored based on settings\nfunc shouldIgnoreSystemFile(filename string) bool {\n\tif setting.GetBool(conf.IgnoreSystemFiles) {\n\t\treturn utils.IsSystemFile(filename)\n\t}\n\treturn false\n}\n\nfunc FsStream(c *gin.Context) {\n\tdefer func() {\n\t\tif n, _ := io.ReadFull(c.Request.Body, []byte{0}); n == 1 {\n\t\t\t_, _ = utils.CopyWithBuffer(io.Discard, c.Request.Body)\n\t\t}\n\t\t_ = c.Request.Body.Close()\n\t}()\n\tpath := c.GetHeader(\"File-Path\")\n\tpath, err := url.PathUnescape(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tasTask := c.GetHeader(\"As-Task\") == \"true\"\n\toverwrite := c.GetHeader(\"Overwrite\") != \"false\"\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tpath, err = user.JoinPath(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !overwrite {\n\t\tif res, _ := fs.Get(c.Request.Context(), path, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\tcommon.ErrorStrResp(c, \"file exists\", 403)\n\t\t\treturn\n\t\t}\n\t}\n\tdir, name := stdpath.Split(path)\n\t// Check if system file should be ignored\n\tif shouldIgnoreSystemFile(name) {\n\t\tcommon.ErrorStrResp(c, errs.IgnoredSystemFile.Error(), 403)\n\t\treturn\n\t}\n\t// 如果请求头 Content-Length 和 X-File-Size 都没有，则 size=-1，表示未知大小的流式上传\n\tsize := c.Request.ContentLength\n\tif size < 0 {\n\t\tsizeStr := c.GetHeader(\"X-File-Size\")\n\t\tif sizeStr != \"\" {\n\t\t\tsize, err = strconv.ParseInt(sizeStr, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\th := make(map[*utils.HashType]string)\n\tif md5 := c.GetHeader(\"X-File-Md5\"); md5 != \"\" {\n\t\th[utils.MD5] = md5\n\t}\n\tif sha1 := c.GetHeader(\"X-File-Sha1\"); sha1 != \"\" {\n\t\th[utils.SHA1] = sha1\n\t}\n\tif sha256 := c.GetHeader(\"X-File-Sha256\"); sha256 != \"\" {\n\t\th[utils.SHA256] = sha256\n\t}\n\tmimetype := c.GetHeader(\"Content-Type\")\n\tif len(mimetype) == 0 {\n\t\tmimetype = utils.GetMimeType(name)\n\t}\n\ts := &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tName:     name,\n\t\t\tSize:     size,\n\t\t\tModified: getLastModified(c),\n\t\t\tHashInfo: utils.NewHashInfoByMap(h),\n\t\t},\n\t\tReader:       c.Request.Body,\n\t\tMimetype:     mimetype,\n\t\tWebPutAsTask: asTask,\n\t}\n\tvar t task.TaskExtensionInfo\n\tif asTask {\n\t\tt, err = fs.PutAsTask(c.Request.Context(), dir, s)\n\t} else {\n\t\terr = fs.PutDirectly(c.Request.Context(), dir, s)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif t == nil {\n\t\tcommon.SuccessResp(c)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"task\": getTaskInfo(t),\n\t})\n}\n\nfunc FsForm(c *gin.Context) {\n\tdefer func() {\n\t\tif n, _ := io.ReadFull(c.Request.Body, []byte{0}); n == 1 {\n\t\t\t_, _ = utils.CopyWithBuffer(io.Discard, c.Request.Body)\n\t\t}\n\t\t_ = c.Request.Body.Close()\n\t}()\n\tpath := c.GetHeader(\"File-Path\")\n\tpath, err := url.PathUnescape(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tasTask := c.GetHeader(\"As-Task\") == \"true\"\n\toverwrite := c.GetHeader(\"Overwrite\") != \"false\"\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tpath, err = user.JoinPath(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !overwrite {\n\t\tif res, _ := fs.Get(c.Request.Context(), path, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\tcommon.ErrorStrResp(c, \"file exists\", 403)\n\t\t\treturn\n\t\t}\n\t}\n\tstorage, err := fs.GetStorage(path, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif storage.Config().NoUpload {\n\t\tcommon.ErrorStrResp(c, \"Current storage doesn't support upload\", 405)\n\t\treturn\n\t}\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tf, err := file.Open()\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tdefer f.Close()\n\tdir, name := stdpath.Split(path)\n\t// Check if system file should be ignored\n\tif shouldIgnoreSystemFile(name) {\n\t\tcommon.ErrorStrResp(c, errs.IgnoredSystemFile.Error(), 403)\n\t\treturn\n\t}\n\th := make(map[*utils.HashType]string)\n\tif md5 := c.GetHeader(\"X-File-Md5\"); md5 != \"\" {\n\t\th[utils.MD5] = md5\n\t}\n\tif sha1 := c.GetHeader(\"X-File-Sha1\"); sha1 != \"\" {\n\t\th[utils.SHA1] = sha1\n\t}\n\tif sha256 := c.GetHeader(\"X-File-Sha256\"); sha256 != \"\" {\n\t\th[utils.SHA256] = sha256\n\t}\n\tmimetype := file.Header.Get(\"Content-Type\")\n\tif len(mimetype) == 0 {\n\t\tmimetype = utils.GetMimeType(name)\n\t}\n\ts := &stream.FileStream{\n\t\tObj: &model.Object{\n\t\t\tName:     name,\n\t\t\tSize:     file.Size,\n\t\t\tModified: getLastModified(c),\n\t\t\tHashInfo: utils.NewHashInfoByMap(h),\n\t\t},\n\t\tReader:       f,\n\t\tMimetype:     mimetype,\n\t\tWebPutAsTask: asTask,\n\t}\n\tvar t task.TaskExtensionInfo\n\tif asTask {\n\t\ts.Reader = struct {\n\t\t\tio.Reader\n\t\t}{f}\n\t\tt, err = fs.PutAsTask(c.Request.Context(), dir, s)\n\t} else {\n\t\terr = fs.PutDirectly(c.Request.Context(), dir, s)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif t == nil {\n\t\tcommon.SuccessResp(c)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"task\": getTaskInfo(t),\n\t})\n}\n"
  },
  {
    "path": "server/handles/helper.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\t\"html\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Favicon(c *gin.Context) {\n\tc.Redirect(302, setting.GetStr(conf.Favicon))\n}\n\nfunc Robots(c *gin.Context) {\n\tc.String(200, setting.GetStr(conf.RobotsTxt))\n}\n\nfunc Plist(c *gin.Context) {\n\tlinkNameB64 := strings.TrimSuffix(c.Param(\"link_name\"), \".plist\")\n\tlinkName, err := utils.SafeAtob(linkNameB64)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tlinkNameSplit := strings.Split(linkName, \"/\")\n\tif len(linkNameSplit) != 2 {\n\t\tcommon.ErrorStrResp(c, \"malformed link\", 400)\n\t\treturn\n\t}\n\tlinkEncode := linkNameSplit[0]\n\tlinkStr, err := url.PathUnescape(linkEncode)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tlink, err := url.Parse(linkStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tnameEncode := linkNameSplit[1]\n\tfullName, err := url.PathUnescape(nameEncode)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tname := fullName\n\tidentifier := fmt.Sprintf(\"org.oplist.%s\", fullName)\n\tif strings.Contains(fullName, \"@\") {\n\t\tss := strings.Split(fullName, \"@\")\n\t\tname = strings.Join(ss[:len(ss)-1], \"@\")\n\t\tidentifier = ss[len(ss)-1]\n\t}\n\tUrl := link.String()\n\tUrl = strings.ReplaceAll(Url, \"<\", \"&lt;\")\n\tUrl = strings.ReplaceAll(Url, \">\", \"&gt;\")\n\tname = html.EscapeString(name)\n\tidentifier = html.EscapeString(identifier)\n\tplist := fmt.Sprintf(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>url</key>\n                        <string><![CDATA[%s]]></string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n\t\t\t\t\t<string>%s</string>\n\t\t\t\t\t<key>bundle-version</key>\n                    <string>4.4</string>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>title</key>\n                    <string>%s</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>`, Url, identifier, name)\n\tc.Header(\"Content-Type\", \"application/xml;charset=utf-8\")\n\tc.Status(200)\n\t_, _ = c.Writer.WriteString(plist)\n}\n"
  },
  {
    "path": "server/handles/index.go",
    "content": "package handles\n\nimport (\n\t\"context\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype UpdateIndexReq struct {\n\tPaths    []string `json:\"paths\"`\n\tMaxDepth int      `json:\"max_depth\"`\n\t//IgnorePaths []string `json:\"ignore_paths\"`\n}\n\nfunc BuildIndex(c *gin.Context) {\n\tif search.Running() {\n\t\tcommon.ErrorStrResp(c, \"index is running\", 400)\n\t\treturn\n\t}\n\tgo func() {\n\t\tctx := context.Background()\n\t\terr := search.Clear(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"clear index error: %+v\", err)\n\t\t\treturn\n\t\t}\n\t\terr = search.BuildIndex(context.Background(), []string{\"/\"},\n\t\t\tconf.SlicesMap[conf.IgnorePaths], setting.GetInt(conf.MaxIndexDepth, 20), true)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"build index error: %+v\", err)\n\t\t}\n\t}()\n\tcommon.SuccessResp(c)\n}\n\nfunc UpdateIndex(c *gin.Context) {\n\tvar req UpdateIndexReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif search.Running() {\n\t\tcommon.ErrorStrResp(c, \"index is running\", 400)\n\t\treturn\n\t}\n\tif !search.Config(c).AutoUpdate {\n\t\tcommon.ErrorStrResp(c, \"update is not supported for current index\", 400)\n\t\treturn\n\t}\n\tgo func() {\n\t\tctx := context.Background()\n\t\tfor _, path := range req.Paths {\n\t\t\terr := search.Del(ctx, path)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"delete index on %s error: %+v\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\terr := search.BuildIndex(context.Background(), req.Paths,\n\t\t\tconf.SlicesMap[conf.IgnorePaths], req.MaxDepth, false)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"update index error: %+v\", err)\n\t\t}\n\t}()\n\tcommon.SuccessResp(c)\n}\n\nfunc StopIndex(c *gin.Context) {\n\tquit := search.Quit.Load()\n\tif quit == nil {\n\t\tcommon.ErrorStrResp(c, \"index is not running\", 400)\n\t\treturn\n\t}\n\tselect {\n\tcase *quit <- struct{}{}:\n\tdefault:\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc ClearIndex(c *gin.Context) {\n\tif search.Running() {\n\t\tcommon.ErrorStrResp(c, \"index is running\", 400)\n\t\treturn\n\t}\n\tsearch.Clear(c)\n\tsearch.WriteProgress(&model.IndexProgress{\n\t\tObjCount:     0,\n\t\tIsDone:       true,\n\t\tLastDoneTime: nil,\n\t\tError:        \"\",\n\t})\n\tcommon.SuccessResp(c)\n}\n\nfunc GetProgress(c *gin.Context) {\n\tprogress, err := search.Progress()\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, progress)\n}\n"
  },
  {
    "path": "server/handles/ldap_login.go",
    "content": "package handles\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc LoginLdap(c *gin.Context) {\n\tvar req LoginReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tenabled := setting.GetBool(conf.LdapLoginEnabled)\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"ldap is not enabled\", 403)\n\t\treturn\n\t}\n\tuser, err := op.GetUserByName(req.Username)\n\tif err == nil && !user.AllowLdap {\n\t\tcommon.ErrorStrResp(c, \"login via ldap is not allowed\", 403)\n\t\treturn\n\t}\n\n\t// check count of login\n\tip := c.ClientIP()\n\tcount, ok := model.LoginCache.Get(ip)\n\tif ok && count >= model.DefaultMaxAuthRetries {\n\t\tcommon.ErrorStrResp(c, \"Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.\", 429)\n\t\tmodel.LoginCache.Expire(ip, model.DefaultLockDuration)\n\t\treturn\n\t}\n\n\terr = common.HandleLdapLogin(req.Username, req.Password)\n\tif err != nil {\n\t\tif errors.Is(err, common.ErrFailedLdapAuth) {\n\t\t\tmodel.LoginCache.Set(ip, count+1)\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t} else {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t}\n\t\treturn\n\t}\n\n\tif user == nil {\n\t\tuser, err = common.LdapRegister(req.Username)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\tmodel.LoginCache.Set(ip, count+1)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// generate token\n\ttoken, err := common.GenerateToken(user)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\"token\": token})\n\tmodel.LoginCache.Del(ip)\n}\n"
  },
  {
    "path": "server/handles/meta.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/dlclark/regexp2\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc ListMetas(c *gin.Context) {\n\tvar req model.PageReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tlog.Debugf(\"%+v\", req)\n\tmetas, total, err := op.GetMetas(req.Page, req.PerPage)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: metas,\n\t\tTotal:   total,\n\t})\n}\n\nfunc CreateMeta(c *gin.Context) {\n\tvar req model.Meta\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tr, err := validHide(req.Hide)\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"%s is illegal: %s\", r, err.Error()), 400)\n\t\treturn\n\t}\n\tif err := op.CreateMeta(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc UpdateMeta(c *gin.Context) {\n\tvar req model.Meta\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tr, err := validHide(req.Hide)\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"%s is illegal: %s\", r, err.Error()), 400)\n\t\treturn\n\t}\n\tif err := op.UpdateMeta(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc validHide(hide string) (string, error) {\n\trs := strings.Split(hide, \"\\n\")\n\tfor _, r := range rs {\n\t\t_, err := regexp2.Compile(r, regexp2.None)\n\t\tif err != nil {\n\t\t\treturn r, err\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\nfunc DeleteMeta(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.DeleteMetaById(uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc GetMeta(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tmeta, err := op.GetMetaById(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, meta)\n}\n"
  },
  {
    "path": "server/handles/offline_download.go",
    "content": "package handles\n\nimport (\n\t\"strings\"\n\n\t_115 \"github.com/OpenListTeam/OpenList/v4/drivers/115\"\n\t_115_open \"github.com/OpenListTeam/OpenList/v4/drivers/115_open\"\n\t_123 \"github.com/OpenListTeam/OpenList/v4/drivers/123\"\n\t_123_open \"github.com/OpenListTeam/OpenList/v4/drivers/123_open\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/pikpak\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser\"\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/thunderx\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype SetAria2Req struct {\n\tUri    string `json:\"uri\" form:\"uri\"`\n\tSecret string `json:\"secret\" form:\"secret\"`\n}\n\nfunc SetAria2(c *gin.Context) {\n\tvar req SetAria2Req\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.Aria2Uri, Value: req.Uri, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.Aria2Secret, Value: req.Secret, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"aria2\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tversion, err := _tool.Init()\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, version)\n}\n\ntype SetQbittorrentReq struct {\n\tUrl      string `json:\"url\" form:\"url\"`\n\tSeedtime string `json:\"seedtime\" form:\"seedtime\"`\n}\n\nfunc SetQbittorrent(c *gin.Context) {\n\tvar req SetQbittorrentReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.QbittorrentUrl, Value: req.Url, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.QbittorrentSeedtime, Value: req.Seedtime, Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"qBittorrent\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype SetTransmissionReq struct {\n\tUri      string `json:\"uri\" form:\"uri\"`\n\tSeedtime string `json:\"seedtime\" form:\"seedtime\"`\n}\n\nfunc SetTransmission(c *gin.Context) {\n\tvar req SetTransmissionReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.TransmissionUri, Value: req.Uri, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.TransmissionSeedtime, Value: req.Seedtime, Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"Transmission\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype Set115Req struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc Set115(c *gin.Context) {\n\tvar req Set115Req\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*_115.Pan115); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only 115 Cloud is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.Pan115TempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"115 Cloud\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype Set115OpenReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc Set115Open(c *gin.Context) {\n\tvar req Set115OpenReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*_115_open.Open115); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only 115 Open is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.Pan115OpenTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"115 Open\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype Set123PanReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc Set123Pan(c *gin.Context) {\n\tvar req Set123PanReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*_123.Pan123); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only 123Pan is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.Pan123TempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"123Pan\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype Set123OpenReq struct {\n\tTempDir     string `json:\"temp_dir\" form:\"temp_dir\"`\n\tCallbackUrl string `json:\"callback_url\" form:\"callback_url\"`\n}\n\nfunc Set123Open(c *gin.Context) {\n\tvar req Set123OpenReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*_123_open.Open123); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only 123 Open is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.Pan123OpenTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t\t{Key: conf.Pan123OpenOfflineDownloadCallbackUrl, Value: req.CallbackUrl, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"123 Open\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype SetPikPakReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc SetPikPak(c *gin.Context) {\n\tvar req SetPikPakReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*pikpak.PikPak); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only PikPak is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.PikPakTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"PikPak\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype SetThunderReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc SetThunder(c *gin.Context) {\n\tvar req SetThunderReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*thunder.Thunder); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only Thunder is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.ThunderTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"Thunder\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype SetThunderXReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc SetThunderX(c *gin.Context) {\n\tvar req SetThunderXReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tif _, ok := storage.(*thunderx.ThunderX); !ok {\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only ThunderX is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.ThunderXTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"ThunderX\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\ntype SetThunderBrowserReq struct {\n\tTempDir string `json:\"temp_dir\" form:\"temp_dir\"`\n}\n\nfunc SetThunderBrowser(c *gin.Context) {\n\tvar req SetThunderBrowserReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.TempDir != \"\" {\n\t\tstorage, _, err := op.GetStorageAndActualPath(req.TempDir)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"storage does not exists\", 400)\n\t\t\treturn\n\t\t}\n\t\tif storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {\n\t\t\tcommon.ErrorStrResp(c, \"storage not init: \"+storage.GetStorage().Status, 400)\n\t\t\treturn\n\t\t}\n\t\tswitch storage.(type) {\n\t\tcase *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert:\n\t\tdefault:\n\t\t\tcommon.ErrorStrResp(c, \"unsupported storage driver for offline download, only ThunderBrowser is supported\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\titems := []model.SettingItem{\n\t\t{Key: conf.ThunderBrowserTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},\n\t}\n\tif err := op.SaveSettingItems(items); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\t_tool, err := tool.Tools.Get(\"ThunderBrowser\")\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif _, err := _tool.Init(); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"ok\")\n}\n\nfunc OfflineDownloadTools(c *gin.Context) {\n\ttools := tool.Tools.Names()\n\tcommon.SuccessResp(c, tools)\n}\n\ntype AddOfflineDownloadReq struct {\n\tUrls         []string `json:\"urls\"`\n\tPath         string   `json:\"path\"`\n\tTool         string   `json:\"tool\"`\n\tDeletePolicy string   `json:\"delete_policy\"`\n}\n\nfunc AddOfflineDownload(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.CanAddOfflineDownloadTasks() {\n\t\tcommon.ErrorStrResp(c, \"permission denied\", 403)\n\t\treturn\n\t}\n\n\tvar req AddOfflineDownloadReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treqPath, err := user.JoinPath(req.Path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tvar tasks []task.TaskExtensionInfo\n\tfor _, url := range req.Urls {\n\t\t// Filter out empty lines and whitespace-only strings\n\t\ttrimmedUrl := strings.TrimSpace(url)\n\t\tif trimmedUrl == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tt, err := tool.AddURL(c, &tool.AddURLArgs{\n\t\t\tURL:          trimmedUrl,\n\t\t\tDstDirPath:   reqPath,\n\t\t\tTool:         req.Tool,\n\t\t\tDeletePolicy: tool.DeletePolicy(req.DeletePolicy),\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tif t != nil {\n\t\t\ttasks = append(tasks, t)\n\t\t}\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"tasks\": getTaskInfos(tasks),\n\t})\n}\n"
  },
  {
    "path": "server/handles/scan.go",
    "content": "package handles\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype ManualScanReq struct {\n\tPath  string  `json:\"path\"`\n\tLimit float64 `json:\"limit\"`\n}\n\nfunc StartManualScan(c *gin.Context) {\n\tvar req ManualScanReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.BeginManualScan(req.Path, req.Limit); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc StopManualScan(c *gin.Context) {\n\tif !op.ManualScanRunning() {\n\t\tcommon.ErrorStrResp(c, \"manual scan is not running\", 400)\n\t\treturn\n\t}\n\top.StopManualScan()\n\tcommon.SuccessResp(c)\n}\n\ntype ManualScanResp struct {\n\tObjCount uint64 `json:\"obj_count\"`\n\tIsDone   bool   `json:\"is_done\"`\n}\n\nfunc GetManualScanProgress(c *gin.Context) {\n\tret := ManualScanResp{\n\t\tObjCount: op.ScannedCount.Load(),\n\t\tIsDone:   !op.ManualScanRunning(),\n\t}\n\tcommon.SuccessResp(c, ret)\n}\n"
  },
  {
    "path": "server/handles/search.go",
    "content": "package handles\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/search\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\ntype SearchReq struct {\n\tmodel.SearchReq\n\tPassword string `json:\"password\"`\n}\n\ntype SearchResp struct {\n\tmodel.SearchNode\n\tType int `json:\"type\"`\n}\n\nfunc Search(c *gin.Context) {\n\tvar (\n\t\treq SearchReq\n\t\terr error\n\t)\n\tif err = c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\treq.Parent, err = user.JoinPath(req.Parent)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := req.Validate(); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tnodes, total, err := search.Search(c, req.SearchReq)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tvar filteredNodes []model.SearchNode\n\tfor _, node := range nodes {\n\t\tif !strings.HasPrefix(node.Parent, user.BasePath) {\n\t\t\tcontinue\n\t\t}\n\t\tmeta, err := op.GetNearestMeta(node.Parent)\n\t\tif err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcontinue\n\t\t}\n\t\tif !common.CanAccess(user, meta, path.Join(node.Parent, node.Name), req.Password) {\n\t\t\tcontinue\n\t\t}\n\t\tfilteredNodes = append(filteredNodes, node)\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),\n\t\tTotal:   total,\n\t})\n}\n\nfunc nodeToSearchResp(node model.SearchNode) SearchResp {\n\treturn SearchResp{\n\t\tSearchNode: node,\n\t\tType:       utils.GetObjType(node.Name, node.IsDir),\n\t}\n}\n"
  },
  {
    "path": "server/handles/setting.go",
    "content": "package handles\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/static\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc ResetToken(c *gin.Context) {\n\ttoken := random.Token()\n\titem := model.SettingItem{Key: \"token\", Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}\n\tif err := op.SaveSettingItem(&item); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tsign.Instance()\n\tcommon.SuccessResp(c, token)\n}\n\nfunc GetSetting(c *gin.Context) {\n\tkey := c.Query(\"key\")\n\tkeys := c.Query(\"keys\")\n\tif key != \"\" {\n\t\titem, err := op.GetSettingItemByKey(key)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tcommon.SuccessResp(c, item)\n\t} else {\n\t\titems, err := op.GetSettingItemInKeys(strings.Split(keys, \",\"))\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tcommon.SuccessResp(c, items)\n\t}\n}\n\nfunc SaveSettings(c *gin.Context) {\n\tvar req []model.SettingItem\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.SaveSettingItems(req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t\tstatic.UpdateIndex()\n\t}\n}\n\nfunc ListSettings(c *gin.Context) {\n\tgroupStr := c.Query(\"group\")\n\tgroupsStr := c.Query(\"groups\")\n\tvar settings []model.SettingItem\n\tvar err error\n\tif groupsStr == \"\" && groupStr == \"\" {\n\t\tsettings, err = op.GetSettingItems()\n\t} else {\n\t\tvar groupStrings []string\n\t\tif groupsStr != \"\" {\n\t\t\tgroupStrings = strings.Split(groupsStr, \",\")\n\t\t} else {\n\t\t\tgroupStrings = append(groupStrings, groupStr)\n\t\t}\n\t\tvar groups []int\n\t\tfor _, str := range groupStrings {\n\t\t\tgroup, err := strconv.Atoi(str)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t\tsettings, err = op.GetSettingItemsInGroups(groups)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, settings)\n}\n\nfunc DefaultSettings(c *gin.Context) {\n\tgroupStr := c.Query(\"group\")\n\tgroupsStr := c.Query(\"groups\")\n\tsettings := data.InitialSettings()\n\tif groupsStr == \"\" && groupStr == \"\" {\n\t\tfor i := range settings {\n\t\t\t(&settings[i]).Index = uint(i)\n\t\t}\n\t\tcommon.SuccessResp(c, settings)\n\t} else {\n\t\tvar groupStrings []string\n\t\tif groupsStr != \"\" {\n\t\t\tgroupStrings = strings.Split(groupsStr, \",\")\n\t\t} else {\n\t\t\tgroupStrings = append(groupStrings, groupStr)\n\t\t}\n\t\tvar groups []int\n\t\tfor _, str := range groupStrings {\n\t\t\tgroup, err := strconv.Atoi(str)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t\tsort.Ints(groups)\n\t\tvar resultItems []model.SettingItem\n\t\tfor _, group := range groups {\n\t\t\tfor i := range settings {\n\t\t\t\titem := settings[i]\n\t\t\t\tif group == item.Group {\n\t\t\t\t\titem.Index = uint(i)\n\t\t\t\t\tresultItems = append(resultItems, item)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcommon.SuccessResp(c, resultItems)\n\t}\n}\n\nfunc DeleteSetting(c *gin.Context) {\n\tkey := c.Query(\"key\")\n\tif err := op.DeleteSettingItemByKey(key); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc PublicSettings(c *gin.Context) {\n\tcommon.SuccessResp(c, op.GetPublicSettingsMap())\n}\n"
  },
  {
    "path": "server/handles/sharing.go",
    "content": "package handles\n\nimport (\n\t\"fmt\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sharing\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/go-cache\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc SharingGet(c *gin.Context, req *FsGetReq) {\n\tsid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, \"/\"), \"/\")\n\tif sid == \"\" {\n\t\tcommon.ErrorStrResp(c, \"invalid share id\", 400)\n\t\treturn\n\t}\n\ts, obj, err := sharing.Get(c.Request.Context(), sid, path, model.SharingListArgs{\n\t\tRefresh: false,\n\t\tPwd:     req.Password,\n\t})\n\tif dealError(c, err) {\n\t\treturn\n\t}\n\t_ = countAccess(c.ClientIP(), s)\n\turl := \"\"\n\tif !obj.IsDir() {\n\t\tfakePath := fmt.Sprintf(\"/%s/%s\", sid, path)\n\t\turl = fmt.Sprintf(\"%s/sd%s\", common.GetApiUrl(c), utils.EncodePath(fakePath, true))\n\t\tif s.Pwd != \"\" {\n\t\t\turl += \"?pwd=\" + s.Pwd\n\t\t}\n\t}\n\tthumb, _ := model.GetThumb(obj)\n\tcommon.SuccessResp(c, FsGetResp{\n\t\tObjResp: ObjResp{\n\t\t\tName:        obj.GetName(),\n\t\t\tSize:        obj.GetSize(),\n\t\t\tIsDir:       obj.IsDir(),\n\t\t\tModified:    obj.ModTime(),\n\t\t\tCreated:     obj.CreateTime(),\n\t\t\tHashInfoStr: obj.GetHash().String(),\n\t\t\tHashInfo:    obj.GetHash().Export(),\n\t\t\tSign:        \"\",\n\t\t\tType:        utils.GetFileType(obj.GetName()),\n\t\t\tThumb:       thumb,\n\t\t},\n\t\tRawURL:   url,\n\t\tReadme:   s.Readme,\n\t\tHeader:   s.Header,\n\t\tProvider: \"unknown\",\n\t\tRelated:  nil,\n\t})\n}\n\nfunc SharingList(c *gin.Context, req *ListReq) {\n\tsid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, \"/\"), \"/\")\n\tif sid == \"\" {\n\t\tcommon.ErrorStrResp(c, \"invalid share id\", 400)\n\t\treturn\n\t}\n\ts, objs, err := sharing.List(c.Request.Context(), sid, path, model.SharingListArgs{\n\t\tRefresh: req.Refresh,\n\t\tPwd:     req.Password,\n\t})\n\tif dealError(c, err) {\n\t\treturn\n\t}\n\t_ = countAccess(c.ClientIP(), s)\n\ttotal, objs := pagination(objs, &req.PageReq)\n\tcommon.SuccessResp(c, FsListResp{\n\t\tContent: utils.MustSliceConvert(objs, func(obj model.Obj) ObjResp {\n\t\t\tthumb, _ := model.GetThumb(obj)\n\t\t\treturn ObjResp{\n\t\t\t\tName:        obj.GetName(),\n\t\t\t\tSize:        obj.GetSize(),\n\t\t\t\tIsDir:       obj.IsDir(),\n\t\t\t\tModified:    obj.ModTime(),\n\t\t\t\tCreated:     obj.CreateTime(),\n\t\t\t\tHashInfoStr: obj.GetHash().String(),\n\t\t\t\tHashInfo:    obj.GetHash().Export(),\n\t\t\t\tSign:        \"\",\n\t\t\t\tThumb:       thumb,\n\t\t\t\tType:        utils.GetObjType(obj.GetName(), obj.IsDir()),\n\t\t\t}\n\t\t}),\n\t\tTotal:    int64(total),\n\t\tReadme:   s.Readme,\n\t\tHeader:   s.Header,\n\t\tWrite:    false,\n\t\tProvider: \"unknown\",\n\t})\n}\n\nfunc SharingArchiveMeta(c *gin.Context, req *ArchiveMetaReq) {\n\tif !setting.GetBool(conf.ShareArchivePreview) {\n\t\tcommon.ErrorStrResp(c, \"sharing archives previewing is not allowed\", 403)\n\t\treturn\n\t}\n\tsid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, \"/\"), \"/\")\n\tif sid == \"\" {\n\t\tcommon.ErrorStrResp(c, \"invalid share id\", 400)\n\t\treturn\n\t}\n\tarchiveArgs := model.ArchiveArgs{\n\t\tLinkArgs: model.LinkArgs{\n\t\t\tHeader: c.Request.Header,\n\t\t\tType:   c.Query(\"type\"),\n\t\t},\n\t\tPassword: req.ArchivePass,\n\t}\n\ts, ret, err := sharing.ArchiveMeta(c.Request.Context(), sid, path, model.SharingArchiveMetaArgs{\n\t\tArchiveMetaArgs: model.ArchiveMetaArgs{\n\t\t\tArchiveArgs: archiveArgs,\n\t\t\tRefresh:     req.Refresh,\n\t\t},\n\t\tPwd: req.Password,\n\t})\n\tif dealError(c, err) {\n\t\treturn\n\t}\n\t_ = countAccess(c.ClientIP(), s)\n\tfakePath := fmt.Sprintf(\"/%s/%s\", sid, path)\n\turl := fmt.Sprintf(\"%s/sad%s\", common.GetApiUrl(c), utils.EncodePath(fakePath, true))\n\tif s.Pwd != \"\" {\n\t\turl += \"?pwd=\" + s.Pwd\n\t}\n\tcommon.SuccessResp(c, ArchiveMetaResp{\n\t\tComment:     ret.GetComment(),\n\t\tIsEncrypted: ret.IsEncrypted(),\n\t\tContent:     toContentResp(ret.GetTree()),\n\t\tSort:        ret.Sort,\n\t\tRawURL:      url,\n\t\tSign:        \"\",\n\t})\n}\n\nfunc SharingArchiveList(c *gin.Context, req *ArchiveListReq) {\n\tif !setting.GetBool(conf.ShareArchivePreview) {\n\t\tcommon.ErrorStrResp(c, \"sharing archives previewing is not allowed\", 403)\n\t\treturn\n\t}\n\tsid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, \"/\"), \"/\")\n\tif sid == \"\" {\n\t\tcommon.ErrorStrResp(c, \"invalid share id\", 400)\n\t\treturn\n\t}\n\tinnerArgs := model.ArchiveInnerArgs{\n\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\tHeader: c.Request.Header,\n\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t},\n\t\t\tPassword: req.ArchivePass,\n\t\t},\n\t\tInnerPath: utils.FixAndCleanPath(req.InnerPath),\n\t}\n\ts, objs, err := sharing.ArchiveList(c.Request.Context(), sid, path, model.SharingArchiveListArgs{\n\t\tArchiveListArgs: model.ArchiveListArgs{\n\t\t\tArchiveInnerArgs: innerArgs,\n\t\t\tRefresh:          req.Refresh,\n\t\t},\n\t\tPwd: req.Password,\n\t})\n\tif dealError(c, err) {\n\t\treturn\n\t}\n\t_ = countAccess(c.ClientIP(), s)\n\ttotal, objs := pagination(objs, &req.PageReq)\n\tret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) {\n\t\treturn toObjsRespWithoutSignAndThumb(src), nil\n\t})\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: ret,\n\t\tTotal:   int64(total),\n\t})\n}\n\nfunc SharingDown(c *gin.Context) {\n\tsid := c.Request.Context().Value(conf.SharingIDKey).(string)\n\tpath := c.Request.Context().Value(conf.PathKey).(string)\n\tpath = utils.FixAndCleanPath(path)\n\tpwd := c.Query(\"pwd\")\n\ts, err := op.GetSharingById(sid)\n\tif err == nil {\n\t\tif !s.Valid() {\n\t\t\terr = errs.InvalidSharing\n\t\t} else if !s.Verify(pwd) {\n\t\t\terr = errs.WrongShareCode\n\t\t} else if len(s.Files) != 1 && path == \"/\" {\n\t\t\terr = errors.New(\"cannot get sharing root link\")\n\t\t}\n\t}\n\tif dealErrorPage(c, err) {\n\t\treturn\n\t}\n\tunwrapPath, err := op.GetSharingUnwrapPath(s, path)\n\tif err != nil {\n\t\tcommon.ErrorPage(c, errors.New(\"failed get sharing unwrap path\"), 500)\n\t\treturn\n\t}\n\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\tif dealErrorPage(c, err) {\n\t\treturn\n\t}\n\tif setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) {\n\t\tif _, ok := c.GetQuery(\"d\"); !ok {\n\t\t\tif url := common.GenerateDownProxyURL(storage.GetStorage(), unwrapPath); url != \"\" {\n\t\t\t\tc.Redirect(302, url)\n\t\t\t\t_ = countAccess(c.ClientIP(), s)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tlink, obj, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{\n\t\t\tHeader: c.Request.Header,\n\t\t\tType:   c.Query(\"type\"),\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, errors.WithMessage(err, \"failed get sharing link\"), 500)\n\t\t\treturn\n\t\t}\n\t\t_ = countAccess(c.ClientIP(), s)\n\t\tproxy(c, link, obj, storage.GetStorage().ProxyRange)\n\t} else {\n\t\tlink, _, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{\n\t\t\tIP:       c.ClientIP(),\n\t\t\tHeader:   c.Request.Header,\n\t\t\tType:     c.Query(\"type\"),\n\t\t\tRedirect: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tcommon.ErrorPage(c, errors.WithMessage(err, \"failed get sharing link\"), 500)\n\t\t\treturn\n\t\t}\n\t\t_ = countAccess(c.ClientIP(), s)\n\t\tredirect(c, link)\n\t}\n}\n\nfunc SharingArchiveExtract(c *gin.Context) {\n\tif !setting.GetBool(conf.ShareArchivePreview) {\n\t\tcommon.ErrorPage(c, errors.New(\"sharing archives previewing is not allowed\"), 403)\n\t\treturn\n\t}\n\tsid := c.Request.Context().Value(conf.SharingIDKey).(string)\n\tpath := c.Request.Context().Value(conf.PathKey).(string)\n\tpath = utils.FixAndCleanPath(path)\n\tpwd := c.Query(\"pwd\")\n\tinnerPath := utils.FixAndCleanPath(c.Query(\"inner\"))\n\tarchivePass := c.Query(\"pass\")\n\ts, err := op.GetSharingById(sid)\n\tif err == nil {\n\t\tif !s.Valid() {\n\t\t\terr = errs.InvalidSharing\n\t\t} else if !s.Verify(pwd) {\n\t\t\terr = errs.WrongShareCode\n\t\t} else if len(s.Files) != 1 && path == \"/\" {\n\t\t\terr = errors.New(\"cannot extract sharing root\")\n\t\t}\n\t}\n\tif dealErrorPage(c, err) {\n\t\treturn\n\t}\n\tunwrapPath, err := op.GetSharingUnwrapPath(s, path)\n\tif err != nil {\n\t\tcommon.ErrorPage(c, errors.New(\"failed get sharing unwrap path\"), 500)\n\t\treturn\n\t}\n\tstorage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)\n\tif dealErrorPage(c, err) {\n\t\treturn\n\t}\n\targs := model.ArchiveInnerArgs{\n\t\tArchiveArgs: model.ArchiveArgs{\n\t\t\tLinkArgs: model.LinkArgs{\n\t\t\t\tHeader: c.Request.Header,\n\t\t\t\tType:   c.Query(\"type\"),\n\t\t\t},\n\t\t\tPassword: archivePass,\n\t\t},\n\t\tInnerPath: innerPath,\n\t}\n\tif _, ok := storage.(driver.ArchiveReader); ok {\n\t\tif setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) {\n\t\t\tlink, obj, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args)\n\t\t\tif dealErrorPage(c, err) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tproxy(c, link, obj, storage.GetStorage().ProxyRange)\n\t\t} else {\n\t\t\targs.Redirect = true\n\t\t\tlink, _, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args)\n\t\t\tif dealErrorPage(c, err) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tredirect(c, link)\n\t\t}\n\t} else {\n\t\trc, size, err := op.InternalExtract(c.Request.Context(), storage, actualPath, args)\n\t\tif dealErrorPage(c, err) {\n\t\t\treturn\n\t\t}\n\t\tfileName := stdpath.Base(innerPath)\n\t\tproxyInternalExtract(c, rc, size, fileName)\n\t}\n}\n\nfunc dealError(c *gin.Context, err error) bool {\n\tif err == nil {\n\t\treturn false\n\t} else if errors.Is(err, errs.SharingNotFound) {\n\t\tcommon.ErrorStrResp(c, \"the share does not exist\", 500)\n\t} else if errors.Is(err, errs.InvalidSharing) {\n\t\tcommon.ErrorStrResp(c, \"the share has expired or is no longer valid\", 500)\n\t} else if errors.Is(err, errs.WrongShareCode) {\n\t\tcommon.ErrorResp(c, err, 403)\n\t} else if errors.Is(err, errs.WrongArchivePassword) {\n\t\tcommon.ErrorResp(c, err, 202)\n\t} else {\n\t\tcommon.ErrorResp(c, err, 500)\n\t}\n\treturn true\n}\n\nfunc dealErrorPage(c *gin.Context, err error) bool {\n\tif err == nil {\n\t\treturn false\n\t} else if errors.Is(err, errs.SharingNotFound) {\n\t\tcommon.ErrorPage(c, errors.New(\"the share does not exist\"), 500)\n\t} else if errors.Is(err, errs.InvalidSharing) {\n\t\tcommon.ErrorPage(c, errors.New(\"the share has expired or is no longer valid\"), 500)\n\t} else if errors.Is(err, errs.WrongShareCode) {\n\t\tcommon.ErrorPage(c, err, 403)\n\t} else if errors.Is(err, errs.WrongArchivePassword) {\n\t\tcommon.ErrorPage(c, err, 202)\n\t} else {\n\t\tcommon.ErrorPage(c, err, 500)\n\t}\n\treturn true\n}\n\ntype SharingResp struct {\n\t*model.Sharing\n\tCreatorName string `json:\"creator\"`\n\tCreatorRole int    `json:\"creator_role\"`\n}\n\nfunc GetSharing(c *gin.Context) {\n\tsid := c.Query(\"id\")\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\ts, err := op.GetSharingById(sid)\n\tif err != nil || (!user.IsAdmin() && s.Creator.ID != user.ID) {\n\t\tcommon.ErrorStrResp(c, \"sharing not found\", 404)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, SharingResp{\n\t\tSharing:     s,\n\t\tCreatorName: s.Creator.Username,\n\t\tCreatorRole: s.Creator.Role,\n\t})\n}\n\nfunc ListSharings(c *gin.Context) {\n\tvar req model.PageReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tvar sharings []model.Sharing\n\tvar total int64\n\tvar err error\n\tif user.IsAdmin() {\n\t\tsharings, total, err = op.GetSharings(req.Page, req.PerPage)\n\t} else {\n\t\tsharings, total, err = op.GetSharingsByCreatorId(user.ID, req.Page, req.PerPage)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: utils.MustSliceConvert(sharings, func(s model.Sharing) SharingResp {\n\t\t\treturn SharingResp{\n\t\t\t\tSharing:     &s,\n\t\t\t\tCreatorName: s.Creator.Username,\n\t\t\t\tCreatorRole: s.Creator.Role,\n\t\t\t}\n\t\t}),\n\t\tTotal: total,\n\t})\n}\n\ntype UpdateSharingReq struct {\n\tFiles       []string   `json:\"files\"`\n\tExpires     *time.Time `json:\"expires\"`\n\tPwd         string     `json:\"pwd\"`\n\tMaxAccessed int        `json:\"max_accessed\"`\n\tDisabled    bool       `json:\"disabled\"`\n\tRemark      string     `json:\"remark\"`\n\tReadme      string     `json:\"readme\"`\n\tHeader      string     `json:\"header\"`\n\tmodel.Sort\n\tCreatorName string `json:\"creator\"`\n\tAccessed    int    `json:\"accessed\"`\n\tID          string `json:\"id\"`\n}\n\nfunc UpdateSharing(c *gin.Context) {\n\tvar req UpdateSharingReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == \"\") {\n\t\tcommon.ErrorStrResp(c, \"must add at least 1 object\", 400)\n\t\treturn\n\t}\n\tvar user *model.User\n\tvar err error\n\treqUser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif reqUser.IsAdmin() && req.CreatorName != \"\" {\n\t\tuser, err = op.GetUserByName(req.CreatorName)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"no such a user\", 400)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tuser = reqUser\n\t\tif !user.CanShare() {\n\t\t\tcommon.ErrorStrResp(c, \"permission denied\", 403)\n\t\t\treturn\n\t\t}\n\t}\n\tfor i, s := range req.Files {\n\t\ts = utils.FixAndCleanPath(s)\n\t\treq.Files[i] = s\n\t\tif !reqUser.IsAdmin() && !strings.HasPrefix(s, user.BasePath) {\n\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"permission denied to share path [%s]\", s), 500)\n\t\t\treturn\n\t\t}\n\t}\n\ts, err := op.GetSharingById(req.ID)\n\tif err != nil || (!reqUser.IsAdmin() && s.CreatorId != user.ID) {\n\t\tcommon.ErrorStrResp(c, \"sharing not found\", 404)\n\t\treturn\n\t}\n\tif reqUser.IsAdmin() && req.CreatorName == \"\" {\n\t\tuser = s.Creator\n\t}\n\ts.Files = req.Files\n\ts.Expires = req.Expires\n\ts.Pwd = req.Pwd\n\ts.Accessed = req.Accessed\n\ts.MaxAccessed = req.MaxAccessed\n\ts.Disabled = req.Disabled\n\ts.Sort = req.Sort\n\ts.Header = req.Header\n\ts.Readme = req.Readme\n\ts.Remark = req.Remark\n\ts.Creator = user\n\tif err = op.UpdateSharing(s); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c, SharingResp{\n\t\t\tSharing:     s,\n\t\t\tCreatorName: s.Creator.Username,\n\t\t\tCreatorRole: s.Creator.Role,\n\t\t})\n\t}\n}\n\nfunc CreateSharing(c *gin.Context) {\n\tvar req UpdateSharingReq\n\tvar err error\n\tif err = c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == \"\") {\n\t\tcommon.ErrorStrResp(c, \"must add at least 1 object\", 400)\n\t\treturn\n\t}\n\tvar user *model.User\n\treqUser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif reqUser.IsAdmin() && req.CreatorName != \"\" {\n\t\tuser, err = op.GetUserByName(req.CreatorName)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"no such a user\", 400)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tuser = reqUser\n\t\tif !user.CanShare() || (!user.IsAdmin() && req.ID != \"\") {\n\t\t\tcommon.ErrorStrResp(c, \"permission denied\", 403)\n\t\t\treturn\n\t\t}\n\t}\n\tfor i, s := range req.Files {\n\t\ts = utils.FixAndCleanPath(s)\n\t\treq.Files[i] = s\n\t\tif !reqUser.IsAdmin() && !strings.HasPrefix(s, user.BasePath) {\n\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"permission denied to share path [%s]\", s), 500)\n\t\t\treturn\n\t\t}\n\t}\n\ts := &model.Sharing{\n\t\tSharingDB: &model.SharingDB{\n\t\t\tID:          req.ID,\n\t\t\tExpires:     req.Expires,\n\t\t\tPwd:         req.Pwd,\n\t\t\tAccessed:    req.Accessed,\n\t\t\tMaxAccessed: req.MaxAccessed,\n\t\t\tDisabled:    req.Disabled,\n\t\t\tSort:        req.Sort,\n\t\t\tRemark:      req.Remark,\n\t\t\tReadme:      req.Readme,\n\t\t\tHeader:      req.Header,\n\t\t},\n\t\tFiles:   req.Files,\n\t\tCreator: user,\n\t}\n\tvar id string\n\tif id, err = op.CreateSharing(s); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\ts.ID = id\n\t\tcommon.SuccessResp(c, SharingResp{\n\t\t\tSharing:     s,\n\t\t\tCreatorName: s.Creator.Username,\n\t\t\tCreatorRole: s.Creator.Role,\n\t\t})\n\t}\n}\n\nfunc DeleteSharing(c *gin.Context) {\n\tsid := c.Query(\"id\")\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\ts, err := op.GetSharingById(sid)\n\tif err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) {\n\t\tcommon.ErrorResp(c, err, 404)\n\t\treturn\n\t}\n\tif err = op.DeleteSharing(sid); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc SetEnableSharing(disable bool) func(ctx *gin.Context) {\n\treturn func(c *gin.Context) {\n\t\tsid := c.Query(\"id\")\n\t\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\t\ts, err := op.GetSharingById(sid)\n\t\tif err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) {\n\t\t\tcommon.ErrorStrResp(c, \"sharing not found\", 404)\n\t\t\treturn\n\t\t}\n\t\ts.Disabled = disable\n\t\tif err = op.UpdateSharing(s, true); err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t} else {\n\t\t\tcommon.SuccessResp(c)\n\t\t}\n\t}\n}\n\nvar (\n\tAccessCache      = cache.NewMemCache[interface{}]()\n\tAccessCountDelay = 30 * time.Minute\n)\n\nfunc countAccess(ip string, s *model.Sharing) error {\n\tkey := fmt.Sprintf(\"%s:%s\", s.ID, ip)\n\t_, ok := AccessCache.Get(key)\n\tif !ok {\n\t\tAccessCache.Set(key, struct{}{}, cache.WithEx[interface{}](AccessCountDelay))\n\t\ts.Accessed += 1\n\t\treturn op.UpdateSharing(s, true)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/handles/sshkey.go",
    "content": "package handles\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype SSHKeyAddReq struct {\n\tTitle string `json:\"title\" binding:\"required\"`\n\tKey   string `json:\"key\" binding:\"required\"`\n}\n\nfunc AddMyPublicKey(c *gin.Context) {\n\tuserObj, ok := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !ok || userObj.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tvar req SSHKeyAddReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorStrResp(c, \"request invalid\", 400)\n\t\treturn\n\t}\n\tif req.Title == \"\" {\n\t\tcommon.ErrorStrResp(c, \"request invalid\", 400)\n\t\treturn\n\t}\n\tkey := &model.SSHPublicKey{\n\t\tTitle:  req.Title,\n\t\tKeyStr: strings.TrimSpace(req.Key),\n\t\tUserId: userObj.ID,\n\t}\n\terr, parsed := op.CreateSSHPublicKey(key)\n\tif !parsed {\n\t\tcommon.ErrorStrResp(c, \"provided key invalid\", 400)\n\t\treturn\n\t} else if err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc ListMyPublicKey(c *gin.Context) {\n\tuserObj, ok := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !ok || userObj.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tlist(c, userObj)\n}\n\nfunc DeleteMyPublicKey(c *gin.Context) {\n\tuserObj, ok := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !ok || userObj.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tkeyId, err := strconv.Atoi(c.Query(\"id\"))\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, \"id format invalid\", 400)\n\t\treturn\n\t}\n\tkey, err := op.GetSSHPublicKeyByIdAndUserId(uint(keyId), userObj.ID)\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, \"failed to get public key\", 404)\n\t\treturn\n\t}\n\terr = op.DeleteSSHPublicKeyById(key.ID)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc ListPublicKeys(c *gin.Context) {\n\tuserId, err := strconv.Atoi(c.Query(\"uid\"))\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, \"user id format invalid\", 400)\n\t\treturn\n\t}\n\tuserObj, err := op.GetUserById(uint(userId))\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 404)\n\t\treturn\n\t}\n\tlist(c, userObj)\n}\n\nfunc DeletePublicKey(c *gin.Context) {\n\tkeyId, err := strconv.Atoi(c.Query(\"id\"))\n\tif err != nil {\n\t\tcommon.ErrorStrResp(c, \"id format invalid\", 400)\n\t\treturn\n\t}\n\terr = op.DeleteSSHPublicKeyById(uint(keyId))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc list(c *gin.Context, userObj *model.User) {\n\tvar req model.PageReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tkeys, total, err := op.GetSSHPublicKeyByUserId(userObj.ID, req.Page, req.PerPage)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: keys,\n\t\tTotal:   total,\n\t})\n}\n"
  },
  {
    "path": "server/handles/ssologin.go",
    "content": "package handles\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/go-cache\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils/random\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/coreos/go-oidc\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"golang.org/x/oauth2\"\n\t\"gorm.io/gorm\"\n)\n\nconst stateLength = 16\nconst stateExpire = time.Minute * 5\n\nvar stateCache = cache.NewMemCache[string](cache.WithShards[string](stateLength))\n\nfunc _keyState(clientID, state string) string {\n\treturn fmt.Sprintf(\"%s_%s\", clientID, state)\n}\n\nfunc generateState(clientID, ip string) string {\n\tstate := random.String(stateLength)\n\tstateCache.Set(_keyState(clientID, state), ip, cache.WithEx[string](stateExpire))\n\treturn state\n}\n\nfunc verifyState(clientID, ip, state string) bool {\n\tvalue, ok := stateCache.Get(_keyState(clientID, state))\n\treturn ok && value == ip\n}\n\nfunc ssoRedirectUri(c *gin.Context, useCompatibility bool, method string) string {\n\tif useCompatibility {\n\t\treturn common.GetApiUrl(c) + \"/api/auth/\" + method\n\t} else {\n\t\treturn common.GetApiUrl(c) + \"/api/auth/sso_callback\" + \"?method=\" + method\n\t}\n}\n\nfunc SSOLoginRedirect(c *gin.Context) {\n\tmethod := c.Query(\"method\")\n\tuseCompatibility := setting.GetBool(conf.SSOCompatibilityMode)\n\tenabled := setting.GetBool(conf.SSOLoginEnabled)\n\tclientId := setting.GetStr(conf.SSOClientId)\n\tplatform := setting.GetStr(conf.SSOLoginPlatform)\n\tvar rUrl string\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"Single sign-on is not enabled\", 403)\n\t\treturn\n\t}\n\turlValues := url.Values{}\n\tif method == \"\" {\n\t\tcommon.ErrorStrResp(c, \"no method provided\", 400)\n\t\treturn\n\t}\n\tredirectUri := ssoRedirectUri(c, useCompatibility, method)\n\turlValues.Add(\"response_type\", \"code\")\n\turlValues.Add(\"redirect_uri\", redirectUri)\n\turlValues.Add(\"client_id\", clientId)\n\tswitch platform {\n\tcase \"Github\":\n\t\trUrl = \"https://github.com/login/oauth/authorize?\"\n\t\turlValues.Add(\"scope\", \"read:user\")\n\tcase \"Microsoft\":\n\t\trUrl = \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?\"\n\t\turlValues.Add(\"scope\", \"user.read\")\n\t\turlValues.Add(\"response_mode\", \"query\")\n\tcase \"Google\":\n\t\trUrl = \"https://accounts.google.com/o/oauth2/v2/auth?\"\n\t\turlValues.Add(\"scope\", \"https://www.googleapis.com/auth/userinfo.profile\")\n\tcase \"Dingtalk\":\n\t\trUrl = \"https://login.dingtalk.com/oauth2/auth?\"\n\t\turlValues.Add(\"scope\", \"openid\")\n\t\turlValues.Add(\"prompt\", \"consent\")\n\t\turlValues.Add(\"response_type\", \"code\")\n\tcase \"Casdoor\":\n\t\tendpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), \"/\")\n\t\trUrl = endpoint + \"/login/oauth/authorize?\"\n\t\turlValues.Add(\"scope\", \"profile\")\n\t\turlValues.Add(\"state\", endpoint)\n\tcase \"OIDC\":\n\t\toauth2Config, err := GetOIDCClient(c, useCompatibility, redirectUri, method)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, err.Error(), 400)\n\t\t\treturn\n\t\t}\n\t\tstate := generateState(clientId, c.ClientIP())\n\t\tc.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(state))\n\t\treturn\n\tdefault:\n\t\tcommon.ErrorStrResp(c, \"invalid platform\", 400)\n\t\treturn\n\t}\n\tc.Redirect(302, rUrl+urlValues.Encode())\n}\n\nvar ssoClient = resty.New().SetRetryCount(3)\n\nfunc GetOIDCClient(c *gin.Context, useCompatibility bool, redirectUri, method string) (*oauth2.Config, error) {\n\tif redirectUri == \"\" {\n\t\tredirectUri = ssoRedirectUri(c, useCompatibility, method)\n\t}\n\tendpoint := setting.GetStr(conf.SSOEndpointName)\n\tprovider, err := oidc.NewProvider(c, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientId := setting.GetStr(conf.SSOClientId)\n\tclientSecret := setting.GetStr(conf.SSOClientSecret)\n\textraScopes := []string{}\n\tif setting.GetStr(conf.SSOExtraScopes) != \"\" {\n\t\textraScopes = strings.Split(setting.GetStr(conf.SSOExtraScopes), \" \")\n\t}\n\treturn &oauth2.Config{\n\t\tClientID:     clientId,\n\t\tClientSecret: clientSecret,\n\t\tRedirectURL:  redirectUri,\n\n\t\t// Discovery returns the OAuth2 endpoints.\n\t\tEndpoint: provider.Endpoint(),\n\n\t\t// \"openid\" is a required scope for OpenID Connect flows.\n\t\tScopes: append([]string{oidc.ScopeOpenID, \"profile\"}, extraScopes...),\n\t}, nil\n}\n\nfunc autoRegister(username, userID string, err error) (*model.User, error) {\n\tif !errors.Is(err, gorm.ErrRecordNotFound) || !setting.GetBool(conf.SSOAutoRegister) {\n\t\treturn nil, err\n\t}\n\tif username == \"\" {\n\t\treturn nil, errors.New(\"cannot get username from SSO provider\")\n\t}\n\tuser := &model.User{\n\t\tID:         0,\n\t\tUsername:   username,\n\t\tPassword:   random.String(16),\n\t\tPermission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),\n\t\tBasePath:   setting.GetStr(conf.SSODefaultDir),\n\t\tRole:       0,\n\t\tDisabled:   false,\n\t\tSsoID:      userID,\n\t}\n\tif err = db.CreateUser(user); err != nil {\n\t\tif strings.HasPrefix(err.Error(), \"UNIQUE constraint failed\") && strings.HasSuffix(err.Error(), \"username\") {\n\t\t\tuser.Username = user.Username + \"_\" + userID\n\t\t\tif err = db.CreateUser(user); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn user, nil\n}\n\nfunc parseJWT(p string) ([]byte, error) {\n\tparts := strings.Split(p, \".\")\n\tif len(parts) < 2 {\n\t\treturn nil, fmt.Errorf(\"oidc: malformed jwt, expected 3 parts got %d\", len(parts))\n\t}\n\tpayload, err := base64.RawURLEncoding.DecodeString(parts[1])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"oidc: malformed jwt payload: %v\", err)\n\t}\n\treturn payload, nil\n}\n\nfunc OIDCLoginCallback(c *gin.Context) {\n\tuseCompatibility := setting.GetBool(conf.SSOCompatibilityMode)\n\tmethod := c.Query(\"method\")\n\tif useCompatibility {\n\t\tmethod = path.Base(c.Request.URL.Path)\n\t}\n\tclientId := setting.GetStr(conf.SSOClientId)\n\tendpoint := setting.GetStr(conf.SSOEndpointName)\n\tprovider, err := oidc.NewProvider(c, endpoint)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\toauth2Config, err := GetOIDCClient(c, useCompatibility, \"\", method)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif !verifyState(clientId, c.ClientIP(), c.Query(\"state\")) {\n\t\tcommon.ErrorStrResp(c, \"incorrect or expired state parameter\", 400)\n\t\treturn\n\t}\n\n\toauth2Token, err := oauth2Config.Exchange(c, c.Query(\"code\"))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\trawIDToken, ok := oauth2Token.Extra(\"id_token\").(string)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"no id_token found in oauth2 token\", 400)\n\t\treturn\n\t}\n\tverifier := provider.Verifier(&oidc.Config{\n\t\tClientID: clientId,\n\t})\n\t_, err = verifier.Verify(c, rawIDToken)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tpayload, err := parseJWT(rawIDToken)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuserID := utils.Json.Get(payload, setting.GetStr(conf.SSOOIDCUsernameKey, \"name\")).ToString()\n\tif userID == \"\" {\n\t\tcommon.ErrorStrResp(c, \"cannot get username from OIDC provider\", 400)\n\t\treturn\n\t}\n\tif method == \"get_sso_id\" {\n\t\tif useCompatibility {\n\t\t\tc.Redirect(302, common.GetApiUrl(c)+\"/@manage?sso_id=\"+userID)\n\t\t\treturn\n\t\t}\n\t\thtml := fmt.Sprintf(`<!DOCTYPE html>\n\t\t\t\t<head></head>\n\t\t\t\t<body>\n\t\t\t\t<script>\n\t\t\t\twindow.opener.postMessage({\"sso_id\": \"%s\"}, \"*\")\n\t\t\t\twindow.close()\n\t\t\t\t</script>\n\t\t\t\t</body>`, userID)\n\t\tc.Data(200, \"text/html; charset=utf-8\", []byte(html))\n\t\treturn\n\t}\n\tif method == \"sso_get_token\" {\n\t\tuser, err := db.GetUserBySSOID(userID)\n\t\tif err != nil {\n\t\t\tuser, err = autoRegister(userID, userID, err)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\ttoken, err := common.GenerateToken(user)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tif useCompatibility {\n\t\t\tc.Redirect(302, common.GetApiUrl(c)+\"/@login?token=\"+token)\n\t\t\treturn\n\t\t}\n\t\thtml := fmt.Sprintf(`<!DOCTYPE html>\n\t\t\t\t<head></head>\n\t\t\t\t<body>\n\t\t\t\t<script>\n\t\t\t\twindow.opener.postMessage({\"token\":\"%s\"}, \"*\")\n\t\t\t\twindow.close()\n\t\t\t\t</script>\n\t\t\t\t</body>`, token)\n\t\tc.Data(200, \"text/html; charset=utf-8\", []byte(html))\n\t\treturn\n\t}\n}\n\nfunc SSOLoginCallback(c *gin.Context) {\n\tenabled := setting.GetBool(conf.SSOLoginEnabled)\n\tusecompatibility := setting.GetBool(conf.SSOCompatibilityMode)\n\tif !enabled {\n\t\tcommon.ErrorResp(c, errors.New(\"sso login is disabled\"), 500)\n\t\treturn\n\t}\n\targument := c.Query(\"method\")\n\tif usecompatibility {\n\t\targument = path.Base(c.Request.URL.Path)\n\t}\n\tif !utils.SliceContains([]string{\"get_sso_id\", \"sso_get_token\"}, argument) {\n\t\tcommon.ErrorResp(c, errors.New(\"invalid request\"), 500)\n\t\treturn\n\t}\n\tclientId := setting.GetStr(conf.SSOClientId)\n\tplatform := setting.GetStr(conf.SSOLoginPlatform)\n\tclientSecret := setting.GetStr(conf.SSOClientSecret)\n\tvar tokenUrl, userUrl, scope, authField, idField, usernameField string\n\tadditionalForm := make(map[string]string)\n\tswitch platform {\n\tcase \"Github\":\n\t\ttokenUrl = \"https://github.com/login/oauth/access_token\"\n\t\tuserUrl = \"https://api.github.com/user\"\n\t\tauthField = \"code\"\n\t\tscope = \"read:user\"\n\t\tidField = \"id\"\n\t\tusernameField = \"login\"\n\tcase \"Microsoft\":\n\t\ttokenUrl = \"https://login.microsoftonline.com/common/oauth2/v2.0/token\"\n\t\tuserUrl = \"https://graph.microsoft.com/v1.0/me\"\n\t\tadditionalForm[\"grant_type\"] = \"authorization_code\"\n\t\tscope = \"user.read\"\n\t\tauthField = \"code\"\n\t\tidField = \"id\"\n\t\tusernameField = \"displayName\"\n\tcase \"Google\":\n\t\ttokenUrl = \"https://oauth2.googleapis.com/token\"\n\t\tuserUrl = \"https://www.googleapis.com/oauth2/v1/userinfo\"\n\t\tadditionalForm[\"grant_type\"] = \"authorization_code\"\n\t\tscope = \"https://www.googleapis.com/auth/userinfo.profile\"\n\t\tauthField = \"code\"\n\t\tidField = \"id\"\n\t\tusernameField = \"name\"\n\tcase \"Dingtalk\":\n\t\ttokenUrl = \"https://api.dingtalk.com/v1.0/oauth2/userAccessToken\"\n\t\tuserUrl = \"https://api.dingtalk.com/v1.0/contact/users/me\"\n\t\tauthField = \"authCode\"\n\t\tidField = \"unionId\"\n\t\tusernameField = \"nick\"\n\tcase \"Casdoor\":\n\t\tendpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), \"/\")\n\t\ttokenUrl = endpoint + \"/api/login/oauth/access_token\"\n\t\tuserUrl = endpoint + \"/api/userinfo\"\n\t\tadditionalForm[\"grant_type\"] = \"authorization_code\"\n\t\tscope = \"profile\"\n\t\tauthField = \"code\"\n\t\tidField = \"sub\"\n\t\tusernameField = \"preferred_username\"\n\tcase \"OIDC\":\n\t\tOIDCLoginCallback(c)\n\t\treturn\n\tdefault:\n\t\tcommon.ErrorStrResp(c, \"invalid platform\", 400)\n\t\treturn\n\t}\n\tcallbackCode := c.Query(authField)\n\tif callbackCode == \"\" {\n\t\tcommon.ErrorStrResp(c, \"No code provided\", 400)\n\t\treturn\n\t}\n\tvar resp *resty.Response\n\tvar err error\n\tif platform == \"Dingtalk\" {\n\t\tresp, err = ssoClient.R().SetHeader(\"content-type\", \"application/json\").SetHeader(\"Accept\", \"application/json\").\n\t\t\tSetBody(map[string]string{\n\t\t\t\t\"clientId\":     clientId,\n\t\t\t\t\"clientSecret\": clientSecret,\n\t\t\t\t\"code\":         callbackCode,\n\t\t\t\t\"grantType\":    \"authorization_code\",\n\t\t\t}).\n\t\t\tPost(tokenUrl)\n\t} else {\n\t\tvar redirect_uri string\n\t\tif usecompatibility {\n\t\t\tredirect_uri = common.GetApiUrl(c) + \"/api/auth/\" + argument\n\t\t} else {\n\t\t\tredirect_uri = common.GetApiUrl(c) + \"/api/auth/sso_callback\" + \"?method=\" + argument\n\t\t}\n\t\tresp, err = ssoClient.R().SetHeader(\"Accept\", \"application/json\").\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"client_id\":     clientId,\n\t\t\t\t\"client_secret\": clientSecret,\n\t\t\t\t\"code\":          callbackCode,\n\t\t\t\t\"redirect_uri\":  redirect_uri,\n\t\t\t\t\"scope\":         scope,\n\t\t\t}).SetFormData(additionalForm).Post(tokenUrl)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif platform == \"Dingtalk\" {\n\t\taccessToken := utils.Json.Get(resp.Body(), \"accessToken\").ToString()\n\t\tresp, err = ssoClient.R().SetHeader(\"x-acs-dingtalk-access-token\", accessToken).\n\t\t\tGet(userUrl)\n\t} else {\n\t\taccessToken := utils.Json.Get(resp.Body(), \"access_token\").ToString()\n\t\tresp, err = ssoClient.R().SetHeader(\"Authorization\", \"Bearer \"+accessToken).\n\t\t\tGet(userUrl)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuserID := utils.Json.Get(resp.Body(), idField).ToString()\n\tif utils.SliceContains([]string{\"\", \"0\"}, userID) {\n\t\tcommon.ErrorResp(c, errors.New(\"error occurred\"), 400)\n\t\treturn\n\t}\n\tif argument == \"get_sso_id\" {\n\t\tif usecompatibility {\n\t\t\tc.Redirect(302, common.GetApiUrl(c)+\"/@manage?sso_id=\"+userID)\n\t\t\treturn\n\t\t}\n\t\thtml := fmt.Sprintf(`<!DOCTYPE html>\n\t\t\t\t<head></head>\n\t\t\t\t<body>\n\t\t\t\t<script>\n\t\t\t\twindow.opener.postMessage({\"sso_id\": \"%s\"}, \"*\")\n\t\t\t\twindow.close()\n\t\t\t\t</script>\n\t\t\t\t</body>`, userID)\n\t\tc.Data(200, \"text/html; charset=utf-8\", []byte(html))\n\t\treturn\n\t}\n\tusername := utils.Json.Get(resp.Body(), usernameField).ToString()\n\tuser, err := db.GetUserBySSOID(userID)\n\tif err != nil {\n\t\tuser, err = autoRegister(username, userID, err)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t}\n\ttoken, err := common.GenerateToken(user)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif usecompatibility {\n\t\tc.Redirect(302, common.GetApiUrl(c)+\"/@login?token=\"+token)\n\t\treturn\n\t}\n\thtml := fmt.Sprintf(`<!DOCTYPE html>\n\t\t\t\t\t\t\t<head></head>\n\t\t\t\t\t\t\t<body>\n\t\t\t\t\t\t\t<script>\n\t\t\t\t\t\t\twindow.opener.postMessage({\"token\":\"%s\"}, \"*\")\n\t\t\t\t\t\t\twindow.close()\n\t\t\t\t\t\t\t</script>\n\t\t\t\t\t\t\t</body>`, token)\n\tc.Data(200, \"text/html; charset=utf-8\", []byte(html))\n}\n"
  },
  {
    "path": "server/handles/storage.go",
    "content": "package handles\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/driver\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype StorageResp struct {\n\tmodel.Storage\n\tMountDetails *model.StorageDetails `json:\"mount_details,omitempty\"`\n}\n\ntype detailWithIndex struct {\n\tidx int\n\tval *model.StorageDetails\n}\n\nfunc makeStorageResp(ctx *gin.Context, storages []model.Storage) []*StorageResp {\n\tret := make([]*StorageResp, len(storages))\n\tdetailsChan := make(chan detailWithIndex, len(storages))\n\tworkerCount := 0\n\tfor i, s := range storages {\n\t\tret[i] = &StorageResp{\n\t\t\tStorage:      s,\n\t\t\tMountDetails: nil,\n\t\t}\n\t\tif setting.GetBool(conf.HideStorageDetailsInManagePage) {\n\t\t\tcontinue\n\t\t}\n\t\td, err := op.GetStorageByMountPath(s.MountPath)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\t_, ok := d.(driver.WithDetails)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tworkerCount++\n\t\tgo func(dri driver.Driver, idx int) {\n\t\t\tdetails, e := op.GetStorageDetails(ctx, dri)\n\t\t\tif e != nil {\n\t\t\t\tif !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {\n\t\t\t\t\tlog.Errorf(\"failed get %s details: %+v\", dri.GetStorage().MountPath, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdetailsChan <- detailWithIndex{idx: idx, val: details}\n\t\t}(d, i)\n\t}\n\tfor workerCount > 0 {\n\t\tselect {\n\t\tcase r := <-detailsChan:\n\t\t\tret[r.idx].MountDetails = r.val\n\t\t\tworkerCount--\n\t\tcase <-time.After(time.Second * 3):\n\t\t\tworkerCount = 0\n\t\t}\n\t}\n\treturn ret\n}\n\nfunc ListStorages(c *gin.Context) {\n\tvar req model.PageReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tlog.Debugf(\"%+v\", req)\n\tstorages, total, err := db.GetStorages(req.Page, req.PerPage)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: makeStorageResp(c, storages),\n\t\tTotal:   total,\n\t})\n}\n\nfunc CreateStorage(c *gin.Context) {\n\tvar req model.Storage\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif id, err := op.CreateStorage(c.Request.Context(), req); err != nil {\n\t\tcommon.ErrorWithDataResp(c, err, 500, gin.H{\n\t\t\t\"id\": id,\n\t\t}, true)\n\t} else {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"id\": id,\n\t\t})\n\t}\n}\n\nfunc UpdateStorage(c *gin.Context) {\n\tvar req model.Storage\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.UpdateStorage(c.Request.Context(), req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc DeleteStorage(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.DeleteStorageById(c.Request.Context(), uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc DisableStorage(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.DisableStorage(c.Request.Context(), uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc EnableStorage(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.EnableStorage(c.Request.Context(), uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc GetStorage(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tstorage, err := db.GetStorageById(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, storage)\n}\n\nfunc LoadAllStorages(c *gin.Context) {\n\tstorages, err := db.GetEnabledStorages()\n\tif err != nil {\n\t\tlog.Errorf(\"failed get enabled storages: %+v\", err)\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tconf.ResetStoragesLoadSignal()\n\tgo func(storages []model.Storage) {\n\t\tfor _, storage := range storages {\n\t\t\tstorageDriver, err := op.GetStorageByMountPath(storage.MountPath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed get storage driver: %+v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// drop the storage in the driver\n\t\t\tif err := storageDriver.Drop(context.Background()); err != nil {\n\t\t\t\tlog.Errorf(\"failed drop storage: %+v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := op.LoadStorage(context.Background(), storage); err != nil {\n\t\t\t\tlog.Errorf(\"failed get enabled storages: %+v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Infof(\"success load storage: [%s], driver: [%s]\",\n\t\t\t\tstorage.MountPath, storage.Driver)\n\t\t}\n\t\tconf.SendStoragesLoadedSignal()\n\t}(storages)\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/task.go",
    "content": "package handles\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/task\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/tache\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype TaskInfo struct {\n\tID          string      `json:\"id\"`\n\tName        string      `json:\"name\"`\n\tCreator     string      `json:\"creator\"`\n\tCreatorRole int         `json:\"creator_role\"`\n\tState       tache.State `json:\"state\"`\n\tStatus      string      `json:\"status\"`\n\tProgress    float64     `json:\"progress\"`\n\tStartTime   *time.Time  `json:\"start_time\"`\n\tEndTime     *time.Time  `json:\"end_time\"`\n\tTotalBytes  int64       `json:\"total_bytes\"`\n\tError       string      `json:\"error\"`\n}\n\nfunc getTaskInfo[T task.TaskExtensionInfo](task T) TaskInfo {\n\terrMsg := \"\"\n\tif task.GetErr() != nil {\n\t\terrMsg = task.GetErr().Error()\n\t}\n\tprogress := task.GetProgress()\n\t// if progress is NaN, set it to 100\n\tif math.IsNaN(progress) {\n\t\tprogress = 100\n\t}\n\tcreatorName := \"\"\n\tcreatorRole := -1\n\tif task.GetCreator() != nil {\n\t\tcreatorName = task.GetCreator().Username\n\t\tcreatorRole = task.GetCreator().Role\n\t}\n\treturn TaskInfo{\n\t\tID:          task.GetID(),\n\t\tName:        task.GetName(),\n\t\tCreator:     creatorName,\n\t\tCreatorRole: creatorRole,\n\t\tState:       task.GetState(),\n\t\tStatus:      task.GetStatus(),\n\t\tProgress:    progress,\n\t\tStartTime:   task.GetStartTime(),\n\t\tEndTime:     task.GetEndTime(),\n\t\tTotalBytes:  task.GetTotalBytes(),\n\t\tError:       errMsg,\n\t}\n}\n\nfunc getTaskInfos[T task.TaskExtensionInfo](tasks []T) []TaskInfo {\n\treturn utils.MustSliceConvert(tasks, getTaskInfo[T])\n}\n\nfunc argsContains[T comparable](v T, slice ...T) bool {\n\treturn utils.SliceContains(slice, v)\n}\n\nfunc getUserInfo(c *gin.Context) (bool, uint, bool) {\n\tif user, ok := c.Request.Context().Value(conf.UserKey).(*model.User); ok {\n\t\treturn user.IsAdmin(), user.ID, true\n\t} else {\n\t\treturn false, 0, false\n\t}\n}\n\nfunc getTargetedHandler[T task.TaskExtensionInfo](manager task.Manager[T], callback func(c *gin.Context, task T)) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tt, ok := manager.GetByID(c.Query(\"tid\"))\n\t\tif !ok {\n\t\t\tcommon.ErrorStrResp(c, \"task not found\", 404)\n\t\t\treturn\n\t\t}\n\t\tif !isAdmin && uid != t.GetCreator().ID {\n\t\t\t// to avoid an attacker using error messages to guess valid TID, return a 404 rather than a 403\n\t\t\tcommon.ErrorStrResp(c, \"task not found\", 404)\n\t\t\treturn\n\t\t}\n\t\tcallback(c, t)\n\t}\n}\n\nfunc getBatchHandler[T task.TaskExtensionInfo](manager task.Manager[T], callback func(task T)) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tvar tids []string\n\t\tif err := c.ShouldBind(&tids); err != nil {\n\t\t\tcommon.ErrorStrResp(c, \"invalid request format\", 400)\n\t\t\treturn\n\t\t}\n\t\tretErrs := make(map[string]string)\n\t\tfor _, tid := range tids {\n\t\t\tt, ok := manager.GetByID(tid)\n\t\t\tif !ok || (!isAdmin && uid != t.GetCreator().ID) {\n\t\t\t\tretErrs[tid] = \"task not found\"\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcallback(t)\n\t\t}\n\t\tcommon.SuccessResp(c, retErrs)\n\t}\n}\n\nfunc taskRoute[T task.TaskExtensionInfo](g *gin.RouterGroup, manager task.Manager[T]) {\n\tg.GET(\"/undone\", func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tcommon.SuccessResp(c, getTaskInfos(manager.GetByCondition(func(task T) bool {\n\t\t\t// avoid directly passing the user object into the function to reduce closure size\n\t\t\treturn (isAdmin || uid == task.GetCreator().ID) &&\n\t\t\t\targsContains(task.GetState(), tache.StatePending, tache.StateRunning, tache.StateCanceling,\n\t\t\t\t\ttache.StateErrored, tache.StateFailing, tache.StateWaitingRetry, tache.StateBeforeRetry)\n\t\t})))\n\t})\n\tg.GET(\"/done\", func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tcommon.SuccessResp(c, getTaskInfos(manager.GetByCondition(func(task T) bool {\n\t\t\treturn (isAdmin || uid == task.GetCreator().ID) &&\n\t\t\t\targsContains(task.GetState(), tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)\n\t\t})))\n\t})\n\tg.POST(\"/info\", getTargetedHandler(manager, func(c *gin.Context, task T) {\n\t\tcommon.SuccessResp(c, getTaskInfo(task))\n\t}))\n\tg.POST(\"/cancel\", getTargetedHandler(manager, func(c *gin.Context, task T) {\n\t\tmanager.Cancel(task.GetID())\n\t\tcommon.SuccessResp(c)\n\t}))\n\tg.POST(\"/delete\", getTargetedHandler(manager, func(c *gin.Context, task T) {\n\t\tmanager.Remove(task.GetID())\n\t\tcommon.SuccessResp(c)\n\t}))\n\tg.POST(\"/retry\", getTargetedHandler(manager, func(c *gin.Context, task T) {\n\t\tmanager.Retry(task.GetID())\n\t\tcommon.SuccessResp(c)\n\t}))\n\tg.POST(\"/cancel_some\", getBatchHandler(manager, func(task T) {\n\t\tmanager.Cancel(task.GetID())\n\t}))\n\tg.POST(\"/delete_some\", getBatchHandler(manager, func(task T) {\n\t\tmanager.Remove(task.GetID())\n\t}))\n\tg.POST(\"/retry_some\", getBatchHandler(manager, func(task T) {\n\t\tmanager.Retry(task.GetID())\n\t}))\n\tg.POST(\"/clear_done\", func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tmanager.RemoveByCondition(func(task T) bool {\n\t\t\treturn (isAdmin || uid == task.GetCreator().ID) &&\n\t\t\t\targsContains(task.GetState(), tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)\n\t\t})\n\t\tcommon.SuccessResp(c)\n\t})\n\tg.POST(\"/clear_succeeded\", func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\tmanager.RemoveByCondition(func(task T) bool {\n\t\t\treturn (isAdmin || uid == task.GetCreator().ID) && task.GetState() == tache.StateSucceeded\n\t\t})\n\t\tcommon.SuccessResp(c)\n\t})\n\tg.POST(\"/retry_failed\", func(c *gin.Context) {\n\t\tisAdmin, uid, ok := getUserInfo(c)\n\t\tif !ok {\n\t\t\t// if there is no bug, here is unreachable\n\t\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\t\treturn\n\t\t}\n\t\ttasks := manager.GetByCondition(func(task T) bool {\n\t\t\treturn (isAdmin || uid == task.GetCreator().ID) && task.GetState() == tache.StateFailed\n\t\t})\n\t\tfor _, t := range tasks {\n\t\t\tmanager.Retry(t.GetID())\n\t\t}\n\t\tcommon.SuccessResp(c)\n\t})\n}\n\nfunc SetupTaskRoute(g *gin.RouterGroup) {\n\ttaskRoute(g.Group(\"/upload\"), fs.UploadTaskManager)\n\ttaskRoute(g.Group(\"/copy\"), fs.CopyTaskManager)\n\ttaskRoute(g.Group(\"/move\"), fs.MoveTaskManager)\n\ttaskRoute(g.Group(\"/offline_download\"), tool.DownloadTaskManager)\n\ttaskRoute(g.Group(\"/offline_download_transfer\"), tool.TransferTaskManager)\n\ttaskRoute(g.Group(\"/decompress\"), fs.ArchiveDownloadTaskManager)\n\ttaskRoute(g.Group(\"/decompress_upload\"), fs.ArchiveContentUploadTaskManager)\n}\n"
  },
  {
    "path": "server/handles/user.go",
    "content": "package handles\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc ListUsers(c *gin.Context) {\n\tvar req model.PageReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\treq.Validate()\n\tlog.Debugf(\"%+v\", req)\n\tusers, total, err := op.GetUsers(req.Page, req.PerPage)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: users,\n\t\tTotal:   total,\n\t})\n}\n\nfunc CreateUser(c *gin.Context) {\n\tvar req model.User\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.IsAdmin() || req.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"admin or guest user can not be created\", 400, true)\n\t\treturn\n\t}\n\treq.SetPassword(req.Password)\n\treq.Password = \"\"\n\treq.Authn = \"[]\"\n\tif err := op.CreateUser(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc UpdateUser(c *gin.Context) {\n\tvar req model.User\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser, err := op.GetUserById(req.ID)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif user.Role != req.Role {\n\t\tcommon.ErrorStrResp(c, \"role can not be changed\", 400)\n\t\treturn\n\t}\n\tif req.Password == \"\" {\n\t\treq.PwdHash = user.PwdHash\n\t\treq.Salt = user.Salt\n\t} else {\n\t\treq.SetPassword(req.Password)\n\t\treq.Password = \"\"\n\t}\n\tif req.OtpSecret == \"\" {\n\t\treq.OtpSecret = user.OtpSecret\n\t}\n\tif req.Disabled && req.IsAdmin() {\n\t\tcommon.ErrorStrResp(c, \"admin user can not be disabled\", 400)\n\t\treturn\n\t}\n\tif err := op.UpdateUser(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc DeleteUser(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.DeleteUserById(uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc GetUser(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser, err := op.GetUserById(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, user)\n}\n\nfunc Cancel2FAById(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.Cancel2FAById(uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc DelUserCache(c *gin.Context) {\n\tusername := c.Query(\"username\")\n\terr := op.DelUserCache(username)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/webauthn.go",
    "content": "package handles\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/authn\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/db\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-webauthn/webauthn/protocol\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n)\n\nfunc BeginAuthnLogin(c *gin.Context) {\n\tenabled := setting.GetBool(conf.WebauthnLoginEnabled)\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"WebAuthn is not enabled\", 403)\n\t\treturn\n\t}\n\tauthnInstance, err := authn.NewAuthnInstance(c)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tvar (\n\t\toptions     *protocol.CredentialAssertion\n\t\tsessionData *webauthn.SessionData\n\t)\n\tif username := c.Query(\"username\"); username != \"\" {\n\t\tvar user *model.User\n\t\tuser, err = db.GetUserByName(username)\n\t\tif err == nil {\n\t\t\toptions, sessionData, err = authnInstance.BeginLogin(user)\n\t\t}\n\t} else { // client-side discoverable login\n\t\toptions, sessionData, err = authnInstance.BeginDiscoverableLogin()\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tval, err := json.Marshal(sessionData)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"options\": options,\n\t\t\"session\": val,\n\t})\n}\n\nfunc FinishAuthnLogin(c *gin.Context) {\n\tenabled := setting.GetBool(conf.WebauthnLoginEnabled)\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"WebAuthn is not enabled\", 403)\n\t\treturn\n\t}\n\tauthnInstance, err := authn.NewAuthnInstance(c)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tsessionDataString := c.GetHeader(\"session\")\n\tsessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tvar sessionData webauthn.SessionData\n\tif err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tvar user *model.User\n\tif username := c.Query(\"username\"); username != \"\" {\n\t\tuser, err = db.GetUserByName(username)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\t_, err = authnInstance.FinishLogin(user, sessionData, c.Request)\n\t} else { // client-side discoverable login\n\t\t_, err = authnInstance.FinishDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) {\n\t\t\t// first param `rawID` in this callback function is equal to ID in webauthn.Credential,\n\t\t\t// but it's unnnecessary to check it.\n\t\t\t// userHandle param is equal to (User).WebAuthnID().\n\t\t\tuserID := uint(binary.LittleEndian.Uint64(userHandle))\n\t\t\tuser, err = db.GetUserById(userID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn user, nil\n\t\t}, sessionData, c.Request)\n\t}\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\ttoken, err := common.GenerateToken(user)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\"token\": token})\n}\n\nfunc BeginAuthnRegistration(c *gin.Context) {\n\tenabled := setting.GetBool(conf.WebauthnLoginEnabled)\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"WebAuthn is not enabled\", 403)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\n\tauthnInstance, err := authn.NewAuthnInstance(c)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\toptions, sessionData, err := authnInstance.BeginRegistration(user)\n\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tval, err := json.Marshal(sessionData)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"options\": options,\n\t\t\"session\": val,\n\t})\n}\n\nfunc FinishAuthnRegistration(c *gin.Context) {\n\tenabled := setting.GetBool(conf.WebauthnLoginEnabled)\n\tif !enabled {\n\t\tcommon.ErrorStrResp(c, \"WebAuthn is not enabled\", 403)\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tsessionDataString := c.GetHeader(\"Session\")\n\n\tauthnInstance, err := authn.NewAuthnInstance(c)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tsessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tvar sessionData webauthn.SessionData\n\tif err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tcredential, err := authnInstance.FinishRegistration(user, sessionData, c.Request)\n\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\terr = db.RegisterAuthn(user, credential)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\terr = op.DelUserCache(user.Username)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"Registered Successfully\")\n}\n\nfunc DeleteAuthnLogin(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\ttype DeleteAuthnReq struct {\n\t\tID string `json:\"id\"`\n\t}\n\tvar req DeleteAuthnReq\n\terr := c.ShouldBind(&req)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\terr = db.RemoveAuthn(user, req.ID)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\terr = op.DelUserCache(user.Username)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, \"Deleted Successfully\")\n}\n\nfunc GetAuthnCredentials(c *gin.Context) {\n\ttype WebAuthnCredentials struct {\n\t\tID          []byte `json:\"id\"`\n\t\tFingerPrint string `json:\"fingerprint\"`\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tcredentials := user.WebAuthnCredentials()\n\tres := make([]WebAuthnCredentials, 0, len(credentials))\n\tfor _, v := range credentials {\n\t\tcredential := WebAuthnCredentials{\n\t\t\tID:          v.ID,\n\t\t\tFingerPrint: fmt.Sprintf(\"% X\", v.Authenticator.AAGUID),\n\t\t}\n\t\tres = append(res, credential)\n\t}\n\tcommon.SuccessResp(c, res)\n}\n"
  },
  {
    "path": "server/middlewares/auth.go",
    "content": "package middlewares\n\nimport (\n\t\"crypto/subtle\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Auth is a middleware that checks if the user is logged in.\n// if token is empty, set user to guest\nfunc Auth(allowDisabledGuest bool) func(c *gin.Context) {\n\treturn func(c *gin.Context) {\n\t\ttoken := c.GetHeader(\"Authorization\")\n\t\tif subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {\n\t\t\tadmin, err := op.GetAdmin()\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcommon.GinWithValue(c, conf.UserKey, admin)\n\t\t\tlog.Debugf(\"use admin token: %+v\", admin)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tif token == \"\" {\n\t\t\tguest, err := op.GetGuest()\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !allowDisabledGuest && guest.Disabled {\n\t\t\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\t\tlog.Debugf(\"use empty token: %+v\", guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tuserClaims, err := common.ParseToken(token)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 401)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tuser, err := op.GetUserByName(userClaims.Username)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 401)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\t// validate password timestamp\n\t\tif userClaims.PwdTS != user.PwdTS {\n\t\t\tcommon.ErrorStrResp(c, \"Password has been changed, login please\", 401)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tif user.Disabled {\n\t\t\tcommon.ErrorStrResp(c, \"Current user is disabled, replace please\", 401)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tcommon.GinWithValue(c, conf.UserKey, user)\n\t\tlog.Debugf(\"use login token: %+v\", user)\n\t\tc.Next()\n\t}\n}\n\nfunc Authn(c *gin.Context) {\n\ttoken := c.GetHeader(\"Authorization\")\n\tif subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {\n\t\tadmin, err := op.GetAdmin()\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tcommon.GinWithValue(c, conf.UserKey, admin)\n\t\tlog.Debugf(\"use admin token: %+v\", admin)\n\t\tc.Next()\n\t\treturn\n\t}\n\tif token == \"\" {\n\t\tguest, err := op.GetGuest()\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\tlog.Debugf(\"use empty token: %+v\", guest)\n\t\tc.Next()\n\t\treturn\n\t}\n\tuserClaims, err := common.ParseToken(token)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 401)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tuser, err := op.GetUserByName(userClaims.Username)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 401)\n\t\tc.Abort()\n\t\treturn\n\t}\n\t// validate password timestamp\n\tif userClaims.PwdTS != user.PwdTS {\n\t\tcommon.ErrorStrResp(c, \"Password has been changed, login please\", 401)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif user.Disabled {\n\t\tcommon.ErrorStrResp(c, \"Current user is disabled, replace please\", 401)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tcommon.GinWithValue(c, conf.UserKey, user)\n\tlog.Debugf(\"use login token: %+v\", user)\n\tc.Next()\n}\n\nfunc AuthNotGuest(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"You are a guest\", 403)\n\t\tc.Abort()\n\t} else {\n\t\tc.Next()\n\t}\n}\n\nfunc AuthAdmin(c *gin.Context) {\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tif !user.IsAdmin() {\n\t\tcommon.ErrorStrResp(c, \"You are not an admin\", 403)\n\t\tc.Abort()\n\t} else {\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "server/middlewares/check.go",
    "content": "package middlewares\n\nimport (\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc StoragesLoaded(c *gin.Context) {\n\tif !conf.StoragesLoaded {\n\t\tif utils.SliceContains([]string{\"\", \"/\", \"/favicon.ico\"}, c.Request.URL.Path) {\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tpaths := []string{\"/assets\", \"/images\", \"/streamer\", \"/static\"}\n\t\tfor _, path := range paths {\n\t\t\tif strings.HasPrefix(c.Request.URL.Path, path) {\n\t\t\t\tc.Next()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tselect {\n\t\tcase <-conf.StoragesLoadSignal():\n\t\tcase <-c.Request.Context().Done():\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t}\n\tcommon.GinWithValue(c,\n\t\tconf.ApiUrlKey, common.GetApiUrlFromRequest(c.Request),\n\t)\n\tc.Next()\n}\n"
  },
  {
    "path": "server/middlewares/down.go",
    "content": "package middlewares\n\nimport (\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc PathParse(c *gin.Context) {\n\trawPath := parsePath(c.Param(\"path\"))\n\tcommon.GinWithValue(c, conf.PathKey, rawPath)\n\tc.Next()\n}\n\nfunc Down(verifyFunc func(string, string) error) func(c *gin.Context) {\n\treturn func(c *gin.Context) {\n\t\trawPath := c.Request.Context().Value(conf.PathKey).(string)\n\t\tmeta, err := op.GetNearestMeta(rawPath)\n\t\tif err != nil {\n\t\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\t\tcommon.ErrorPage(c, err, 500, true)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tcommon.GinWithValue(c, conf.MetaKey, meta)\n\t\t// verify sign\n\t\tif needSign(meta, rawPath) {\n\t\t\ts := c.Query(\"sign\")\n\t\t\terr = verifyFunc(rawPath, strings.TrimSuffix(s, \"/\"))\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorPage(c, err, 401)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tc.Next()\n\t}\n}\n\n// TODO: implement\n// path maybe contains # ? etc.\nfunc parsePath(path string) string {\n\treturn utils.FixAndCleanPath(path)\n}\n\nfunc needSign(meta *model.Meta, path string) bool {\n\tif setting.GetBool(conf.SignAll) {\n\t\treturn true\n\t}\n\tif common.IsStorageSignEnabled(path) {\n\t\treturn true\n\t}\n\tif meta == nil || meta.Password == \"\" {\n\t\treturn false\n\t}\n\tif !meta.PSub && path != meta.Path {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "server/middlewares/filtered_logger.go",
    "content": "package middlewares\n\nimport (\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype filter struct {\n\tCIDR   *netip.Prefix `json:\"cidr,omitempty\"`\n\tPath   *string       `json:\"path,omitempty\"`\n\tMethod *string       `json:\"method,omitempty\"`\n}\n\nvar filterList []*filter\n\nfunc initFilterList() {\n\tfor _, s := range conf.Conf.Log.Filter.Filters {\n\t\tf := new(filter)\n\n\t\tif s.CIDR != \"\" {\n\t\t\tcidr, err := netip.ParsePrefix(s.CIDR)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to parse CIDR %s: %v\", s.CIDR, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tf.CIDR = &cidr\n\t\t}\n\n\t\tif s.Path != \"\" {\n\t\t\tf.Path = &s.Path\n\t\t}\n\n\t\tif s.Method != \"\" {\n\t\t\tf.Method = &s.Method\n\t\t}\n\n\t\tif f.CIDR == nil && f.Path == nil && f.Method == nil {\n\t\t\tlog.Warnf(\"filter %s is empty, skipping\", s)\n\t\t\tcontinue\n\t\t}\n\n\t\tfilterList = append(filterList, f)\n\t\tlog.Debugf(\"added filter: %+v\", f)\n\t}\n\n\tlog.Infof(\"Loaded %d log filters.\", len(filterList))\n}\n\nfunc skiperDecider(c *gin.Context) bool {\n\t// every filter need metch all condithon as filter match\n\t// so if any condithon not metch, skip this filter\n\t// all filters misatch, log this request\n\n\tfor _, f := range filterList {\n\t\tif f.CIDR != nil {\n\t\t\tcip := netip.MustParseAddr(c.ClientIP())\n\t\t\tif !f.CIDR.Contains(cip) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif f.Path != nil {\n\t\t\tif (*f.Path)[0] == '/' {\n\t\t\t\t// match path as prefix/exact path\n\t\t\t\tif !strings.HasPrefix(c.Request.URL.Path, *f.Path) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// match path as relative path\n\t\t\t\tif !strings.Contains(c.Request.URL.Path, \"/\"+*f.Path) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif f.Method != nil {\n\t\t\tif *f.Method != c.Request.Method {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc FilteredLogger() gin.HandlerFunc {\n\tinitFilterList()\n\n\treturn gin.LoggerWithConfig(gin.LoggerConfig{\n\t\tOutput: log.StandardLogger().Out,\n\t\tSkip:   skiperDecider,\n\t})\n}\n"
  },
  {
    "path": "server/middlewares/fsup.go",
    "content": "package middlewares\n\nimport (\n\t\"net/url\"\n\tstdpath \"path\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc FsUp(c *gin.Context) {\n\tpath := c.GetHeader(\"File-Path\")\n\tpassword := c.GetHeader(\"Password\")\n\tpath, err := url.PathUnescape(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tuser := c.Request.Context().Value(conf.UserKey).(*model.User)\n\tpath, err = user.JoinPath(path)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tmeta, err := op.GetNearestMeta(stdpath.Dir(path))\n\tif err != nil {\n\t\tif !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\tcommon.ErrorResp(c, err, 500, true)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t}\n\tif !(common.CanAccess(user, meta, path, password) && (user.CanWrite() || common.CanWrite(meta, stdpath.Dir(path)))) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tc.Next()\n}\n"
  },
  {
    "path": "server/middlewares/https.go",
    "content": "package middlewares\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc ForceHttps(c *gin.Context) {\n\tif c.Request.TLS == nil {\n\t\thost := c.Request.Host\n\t\t// change port to https port\n\t\thost = strings.Replace(host, fmt.Sprintf(\":%d\", conf.Conf.Scheme.HttpPort), fmt.Sprintf(\":%d\", conf.Conf.Scheme.HttpsPort), 1)\n\t\tc.Redirect(302, \"https://\"+host+c.Request.RequestURI)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tc.Next()\n}\n"
  },
  {
    "path": "server/middlewares/limit.go",
    "content": "package middlewares\n\nimport (\n\t\"io\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc MaxAllowed(n int) gin.HandlerFunc {\n\tsem := make(chan struct{}, n)\n\tacquire := func() { sem <- struct{}{} }\n\trelease := func() { <-sem }\n\treturn func(c *gin.Context) {\n\t\tacquire()\n\t\tdefer release()\n\t\tc.Next()\n\t}\n}\n\nfunc UploadRateLimiter(limiter stream.Limiter) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Request.Body = &stream.RateLimitReader{\n\t\t\tReader:  c.Request.Body,\n\t\t\tLimiter: limiter,\n\t\t\tCtx:     c,\n\t\t}\n\t\tc.Next()\n\t}\n}\n\ntype ResponseWriterWrapper struct {\n\tgin.ResponseWriter\n\tWrapWriter io.Writer\n}\n\nfunc (w *ResponseWriterWrapper) Write(p []byte) (n int, err error) {\n\treturn w.WrapWriter.Write(p)\n}\n\nfunc DownloadRateLimiter(limiter stream.Limiter) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Writer = &ResponseWriterWrapper{\n\t\t\tResponseWriter: c.Writer,\n\t\t\tWrapWriter: &stream.RateLimitWriter{\n\t\t\t\tWriter:  c.Writer,\n\t\t\t\tLimiter: limiter,\n\t\t\t\tCtx:     c,\n\t\t\t},\n\t\t}\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "server/middlewares/search.go",
    "content": "package middlewares\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc SearchIndex(c *gin.Context) {\n\tmode := setting.GetStr(conf.SearchIndex)\n\tif mode == \"none\" {\n\t\tcommon.ErrorResp(c, errs.SearchNotAvailable, 404)\n\t\tc.Abort()\n\t} else {\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "server/middlewares/sharing.go",
    "content": "package middlewares\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc SharingIdParse(c *gin.Context) {\n\tsid := c.Param(\"sid\")\n\tcommon.GinWithValue(c, conf.SharingIDKey, sid)\n\tc.Next()\n}\n\nfunc EmptyPathParse(c *gin.Context) {\n\tcommon.GinWithValue(c, conf.PathKey, \"/\")\n\tc.Next()\n}\n"
  },
  {
    "path": "server/router.go",
    "content": "package server\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/message\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/sign\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/handles\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/middlewares\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/static\"\n\t\"github.com/gin-contrib/cors\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Init(e *gin.Engine) {\n\te.ContextWithFallback = true\n\tif !utils.SliceContains([]string{\"\", \"/\"}, conf.URL.Path) {\n\t\te.GET(\"/\", func(c *gin.Context) {\n\t\t\tc.Redirect(302, conf.URL.Path)\n\t\t})\n\t}\n\tCors(e)\n\tg := e.Group(conf.URL.Path)\n\tif conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {\n\t\te.Use(middlewares.ForceHttps)\n\t}\n\tg.Any(\"/ping\", func(c *gin.Context) {\n\t\tc.String(200, \"pong\")\n\t})\n\tg.GET(\"/favicon.ico\", handles.Favicon)\n\tg.GET(\"/robots.txt\", handles.Robots)\n\tg.GET(\"/manifest.json\", static.ManifestJSON)\n\tg.GET(\"/i/:link_name\", handles.Plist)\n\tcommon.SecretKey = []byte(conf.Conf.JwtSecret)\n\tg.Use(middlewares.StoragesLoaded)\n\tif conf.Conf.MaxConnections > 0 {\n\t\tg.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections))\n\t}\n\tWebDav(g.Group(\"/dav\"))\n\tS3(g.Group(\"/s3\"))\n\n\tdownloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit)\n\tsignCheck := middlewares.Down(sign.Verify)\n\tg.GET(\"/d/*path\", middlewares.PathParse, signCheck, downloadLimiter, handles.Down)\n\tg.GET(\"/p/*path\", middlewares.PathParse, signCheck, downloadLimiter, handles.Proxy)\n\tg.HEAD(\"/d/*path\", middlewares.PathParse, signCheck, handles.Down)\n\tg.HEAD(\"/p/*path\", middlewares.PathParse, signCheck, handles.Proxy)\n\tarchiveSignCheck := middlewares.Down(sign.VerifyArchive)\n\tg.GET(\"/ad/*path\", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown)\n\tg.GET(\"/ap/*path\", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveProxy)\n\tg.GET(\"/ae/*path\", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)\n\tg.HEAD(\"/ad/*path\", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown)\n\tg.HEAD(\"/ap/*path\", middlewares.PathParse, archiveSignCheck, handles.ArchiveProxy)\n\tg.HEAD(\"/ae/*path\", middlewares.PathParse, archiveSignCheck, handles.ArchiveInternalExtract)\n\n\tg.GET(\"/sd/:sid\", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)\n\tg.GET(\"/sd/:sid/*path\", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)\n\tg.HEAD(\"/sd/:sid\", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingDown)\n\tg.HEAD(\"/sd/:sid/*path\", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingDown)\n\tg.GET(\"/sad/:sid\", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)\n\tg.GET(\"/sad/:sid/*path\", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)\n\tg.HEAD(\"/sad/:sid\", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)\n\tg.HEAD(\"/sad/:sid/*path\", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)\n\n\tapi := g.Group(\"/api\")\n\tauth := api.Group(\"\", middlewares.Auth(false))\n\twebauthn := api.Group(\"/authn\", middlewares.Authn)\n\n\tapi.POST(\"/auth/login\", handles.Login)\n\tapi.POST(\"/auth/login/hash\", handles.LoginHash)\n\tapi.POST(\"/auth/login/ldap\", handles.LoginLdap)\n\tauth.GET(\"/me\", handles.CurrentUser)\n\tauth.POST(\"/me/update\", handles.UpdateCurrent)\n\tauth.GET(\"/me/sshkey/list\", handles.ListMyPublicKey)\n\tauth.POST(\"/me/sshkey/add\", handles.AddMyPublicKey)\n\tauth.POST(\"/me/sshkey/delete\", handles.DeleteMyPublicKey)\n\tauth.POST(\"/auth/2fa/generate\", handles.Generate2FA)\n\tauth.POST(\"/auth/2fa/verify\", handles.Verify2FA)\n\tauth.GET(\"/auth/logout\", handles.LogOut)\n\n\t// auth\n\tapi.GET(\"/auth/sso\", handles.SSOLoginRedirect)\n\tapi.GET(\"/auth/sso_callback\", handles.SSOLoginCallback)\n\tapi.GET(\"/auth/get_sso_id\", handles.SSOLoginCallback)\n\tapi.GET(\"/auth/sso_get_token\", handles.SSOLoginCallback)\n\n\t// webauthn\n\tapi.GET(\"/authn/webauthn_begin_login\", handles.BeginAuthnLogin)\n\tapi.POST(\"/authn/webauthn_finish_login\", handles.FinishAuthnLogin)\n\twebauthn.GET(\"/webauthn_begin_registration\", handles.BeginAuthnRegistration)\n\twebauthn.POST(\"/webauthn_finish_registration\", handles.FinishAuthnRegistration)\n\twebauthn.POST(\"/delete_authn\", handles.DeleteAuthnLogin)\n\twebauthn.GET(\"/getcredentials\", handles.GetAuthnCredentials)\n\n\t// no need auth\n\tpublic := api.Group(\"/public\")\n\tpublic.Any(\"/settings\", handles.PublicSettings)\n\tpublic.Any(\"/offline_download_tools\", handles.OfflineDownloadTools)\n\tpublic.Any(\"/archive_extensions\", handles.ArchiveExtensions)\n\n\t_fs(auth.Group(\"/fs\"))\n\tfsAndShare(api.Group(\"/fs\", middlewares.Auth(true)))\n\t_task(auth.Group(\"/task\", middlewares.AuthNotGuest))\n\t_sharing(auth.Group(\"/share\", middlewares.AuthNotGuest))\n\tadmin(auth.Group(\"/admin\", middlewares.AuthAdmin))\n\tif flags.Debug || flags.Dev {\n\t\tdebug(g.Group(\"/debug\"))\n\t}\n\tstatic.Static(g, func(handlers ...gin.HandlerFunc) {\n\t\te.NoRoute(handlers...)\n\t})\n}\n\nfunc admin(g *gin.RouterGroup) {\n\tmeta := g.Group(\"/meta\")\n\tmeta.GET(\"/list\", handles.ListMetas)\n\tmeta.GET(\"/get\", handles.GetMeta)\n\tmeta.POST(\"/create\", handles.CreateMeta)\n\tmeta.POST(\"/update\", handles.UpdateMeta)\n\tmeta.POST(\"/delete\", handles.DeleteMeta)\n\n\tuser := g.Group(\"/user\")\n\tuser.GET(\"/list\", handles.ListUsers)\n\tuser.GET(\"/get\", handles.GetUser)\n\tuser.POST(\"/create\", handles.CreateUser)\n\tuser.POST(\"/update\", handles.UpdateUser)\n\tuser.POST(\"/cancel_2fa\", handles.Cancel2FAById)\n\tuser.POST(\"/delete\", handles.DeleteUser)\n\tuser.POST(\"/del_cache\", handles.DelUserCache)\n\tuser.GET(\"/sshkey/list\", handles.ListPublicKeys)\n\tuser.POST(\"/sshkey/delete\", handles.DeletePublicKey)\n\n\tstorage := g.Group(\"/storage\")\n\tstorage.GET(\"/list\", handles.ListStorages)\n\tstorage.GET(\"/get\", handles.GetStorage)\n\tstorage.POST(\"/create\", handles.CreateStorage)\n\tstorage.POST(\"/update\", handles.UpdateStorage)\n\tstorage.POST(\"/delete\", handles.DeleteStorage)\n\tstorage.POST(\"/enable\", handles.EnableStorage)\n\tstorage.POST(\"/disable\", handles.DisableStorage)\n\tstorage.POST(\"/load_all\", handles.LoadAllStorages)\n\n\tdriver := g.Group(\"/driver\")\n\tdriver.GET(\"/list\", handles.ListDriverInfo)\n\tdriver.GET(\"/names\", handles.ListDriverNames)\n\tdriver.GET(\"/info\", handles.GetDriverInfo)\n\n\tsetting := g.Group(\"/setting\")\n\tsetting.GET(\"/get\", handles.GetSetting)\n\tsetting.GET(\"/list\", handles.ListSettings)\n\tsetting.POST(\"/save\", handles.SaveSettings)\n\tsetting.POST(\"/delete\", handles.DeleteSetting)\n\tsetting.POST(\"/default\", handles.DefaultSettings)\n\tsetting.POST(\"/reset_token\", handles.ResetToken)\n\tsetting.POST(\"/set_aria2\", handles.SetAria2)\n\tsetting.POST(\"/set_qbit\", handles.SetQbittorrent)\n\tsetting.POST(\"/set_transmission\", handles.SetTransmission)\n\tsetting.POST(\"/set_115\", handles.Set115)\n\tsetting.POST(\"/set_115_open\", handles.Set115Open)\n\tsetting.POST(\"/set_123_pan\", handles.Set123Pan)\n\tsetting.POST(\"/set_123_open\", handles.Set123Open)\n\tsetting.POST(\"/set_pikpak\", handles.SetPikPak)\n\tsetting.POST(\"/set_thunder\", handles.SetThunder)\n\tsetting.POST(\"/set_thunderx\", handles.SetThunderX)\n\tsetting.POST(\"/set_thunder_browser\", handles.SetThunderBrowser)\n\n\t// retain /admin/task API to ensure compatibility with legacy automation scripts\n\t_task(g.Group(\"/task\"))\n\n\tms := g.Group(\"/message\")\n\tms.POST(\"/get\", message.HttpInstance.GetHandle)\n\tms.POST(\"/send\", message.HttpInstance.SendHandle)\n\n\tindex := g.Group(\"/index\")\n\tindex.POST(\"/build\", middlewares.SearchIndex, handles.BuildIndex)\n\tindex.POST(\"/update\", middlewares.SearchIndex, handles.UpdateIndex)\n\tindex.POST(\"/stop\", middlewares.SearchIndex, handles.StopIndex)\n\tindex.POST(\"/clear\", middlewares.SearchIndex, handles.ClearIndex)\n\tindex.GET(\"/progress\", middlewares.SearchIndex, handles.GetProgress)\n\n\tscan := g.Group(\"/scan\")\n\tscan.POST(\"/start\", handles.StartManualScan)\n\tscan.POST(\"/stop\", handles.StopManualScan)\n\tscan.GET(\"/progress\", handles.GetManualScanProgress)\n}\n\nfunc fsAndShare(g *gin.RouterGroup) {\n\tg.Any(\"/list\", handles.FsListSplit)\n\tg.Any(\"/get\", handles.FsGetSplit)\n\ta := g.Group(\"/archive\")\n\ta.Any(\"/meta\", handles.FsArchiveMetaSplit)\n\ta.Any(\"/list\", handles.FsArchiveListSplit)\n}\n\nfunc _fs(g *gin.RouterGroup) {\n\tg.Any(\"/search\", middlewares.SearchIndex, handles.Search)\n\tg.Any(\"/other\", handles.FsOther)\n\tg.Any(\"/dirs\", handles.FsDirs)\n\tg.POST(\"/mkdir\", handles.FsMkdir)\n\tg.POST(\"/rename\", handles.FsRename)\n\tg.POST(\"/batch_rename\", handles.FsBatchRename)\n\tg.POST(\"/regex_rename\", handles.FsRegexRename)\n\tg.POST(\"/move\", handles.FsMove)\n\tg.POST(\"/recursive_move\", handles.FsRecursiveMove)\n\tg.POST(\"/copy\", handles.FsCopy)\n\tg.POST(\"/remove\", handles.FsRemove)\n\tg.POST(\"/remove_empty_directory\", handles.FsRemoveEmptyDirectory)\n\tuploadLimiter := middlewares.UploadRateLimiter(stream.ClientUploadLimit)\n\tg.PUT(\"/put\", middlewares.FsUp, uploadLimiter, handles.FsStream)\n\tg.PUT(\"/form\", middlewares.FsUp, uploadLimiter, handles.FsForm)\n\tg.POST(\"/link\", middlewares.AuthAdmin, handles.Link)\n\t// g.POST(\"/add_aria2\", handles.AddOfflineDownload)\n\t// g.POST(\"/add_qbit\", handles.AddQbittorrent)\n\t// g.POST(\"/add_transmission\", handles.SetTransmission)\n\tg.POST(\"/add_offline_download\", handles.AddOfflineDownload)\n\tg.POST(\"/archive/decompress\", handles.FsArchiveDecompress)\n\t// Direct upload (client-side upload to storage)\n\tg.POST(\"/get_direct_upload_info\", middlewares.FsUp, handles.FsGetDirectUploadInfo)\n}\n\nfunc _task(g *gin.RouterGroup) {\n\thandles.SetupTaskRoute(g)\n}\n\nfunc _sharing(g *gin.RouterGroup) {\n\tg.Any(\"/list\", handles.ListSharings)\n\tg.GET(\"/get\", handles.GetSharing)\n\tg.POST(\"/create\", handles.CreateSharing)\n\tg.POST(\"/update\", handles.UpdateSharing)\n\tg.POST(\"/delete\", handles.DeleteSharing)\n\tg.POST(\"/enable\", handles.SetEnableSharing(false))\n\tg.POST(\"/disable\", handles.SetEnableSharing(true))\n}\n\nfunc Cors(r *gin.Engine) {\n\tconfig := cors.DefaultConfig()\n\t// config.AllowAllOrigins = true\n\tconfig.AllowOrigins = conf.Conf.Cors.AllowOrigins\n\tconfig.AllowHeaders = conf.Conf.Cors.AllowHeaders\n\tconfig.AllowMethods = conf.Conf.Cors.AllowMethods\n\tr.Use(cors.New(config))\n}\n\nfunc InitS3(e *gin.Engine) {\n\tCors(e)\n\tS3Server(e.Group(\"/\"))\n}\n"
  },
  {
    "path": "server/s3/backend.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/http_range\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/itsHenry35/gofakes3\"\n\t\"github.com/ncw/swift/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\temptyPrefix = &gofakes3.Prefix{}\n\ttimeFormat  = \"Mon, 2 Jan 2006 15:04:05 GMT\"\n)\n\n// s3Backend implements the gofacess3.Backend interface to make an S3\n// backend for gofakes3\ntype s3Backend struct {\n\tmeta *sync.Map\n}\n\n// newBackend creates a new SimpleBucketBackend.\nfunc newBackend() gofakes3.Backend {\n\treturn &s3Backend{\n\t\tmeta: new(sync.Map),\n\t}\n}\n\n// ListBuckets always returns the default bucket.\nfunc (b *s3Backend) ListBuckets(ctx context.Context) ([]gofakes3.BucketInfo, error) {\n\tbuckets, err := getAndParseBuckets()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar response []gofakes3.BucketInfo\n\tfor _, b := range buckets {\n\t\tnode, _ := fs.Get(ctx, b.Path, &fs.GetArgs{})\n\t\tresponse = append(response, gofakes3.BucketInfo{\n\t\t\t// Name:         gofakes3.URLEncode(b.Name),\n\t\t\tName:         b.Name,\n\t\t\tCreationDate: gofakes3.NewContentTime(node.ModTime()),\n\t\t})\n\t}\n\treturn response, nil\n}\n\n// ListBucket lists the objects in the given bucket.\nfunc (b *s3Backend) ListBucket(ctx context.Context, bucketName string, prefix *gofakes3.Prefix, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {\n\tbucket, err := getBucketByName(bucketName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbucketPath := bucket.Path\n\n\tif prefix == nil {\n\t\tprefix = emptyPrefix\n\t}\n\n\t// workaround\n\tif strings.TrimSpace(prefix.Prefix) == \"\" {\n\t\tprefix.HasPrefix = false\n\t}\n\tif strings.TrimSpace(prefix.Delimiter) == \"\" {\n\t\tprefix.HasDelimiter = false\n\t}\n\n\tresponse := gofakes3.NewObjectList()\n\tpath, remaining := prefixParser(prefix)\n\n\terr = b.entryListR(bucketPath, path, remaining, prefix.HasDelimiter, response)\n\tif err == gofakes3.ErrNoSuchKey {\n\t\t// AWS just returns an empty list\n\t\tresponse = gofakes3.NewObjectList()\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b.pager(response, page)\n}\n\n// HeadObject returns the fileinfo for the given object name.\n//\n// Note that the metadata is not supported yet.\nfunc (b *s3Backend) HeadObject(ctx context.Context, bucketName, objectName string) (*gofakes3.Object, error) {\n\tbucket, err := getBucketByName(bucketName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbucketPath := bucket.Path\n\n\tfp := path.Join(bucketPath, objectName)\n\tfmeta, _ := op.GetNearestMeta(fp)\n\tnode, err := fs.Get(context.WithValue(ctx, conf.MetaKey, fmeta), fp, &fs.GetArgs{})\n\tif err != nil {\n\t\treturn nil, gofakes3.KeyNotFound(objectName)\n\t}\n\n\tif node.IsDir() {\n\t\treturn nil, gofakes3.KeyNotFound(objectName)\n\t}\n\n\tsize := node.GetSize()\n\t// hash := getFileHashByte(fobj)\n\n\tmeta := map[string]string{\n\t\t\"Last-Modified\": node.ModTime().Format(timeFormat),\n\t\t\"Content-Type\":  utils.GetMimeType(fp),\n\t}\n\n\tif val, ok := b.meta.Load(fp); ok {\n\t\tmetaMap := val.(map[string]string)\n\t\tfor k, v := range metaMap {\n\t\t\tmeta[k] = v\n\t\t}\n\t}\n\n\treturn &gofakes3.Object{\n\t\tName: objectName,\n\t\t// Hash:     hash,\n\t\tMetadata: meta,\n\t\tSize:     size,\n\t\tContents: noOpReadCloser{},\n\t}, nil\n}\n\n// GetObject fetchs the object from the filesystem.\nfunc (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string, rangeRequest *gofakes3.ObjectRangeRequest) (s3Obj *gofakes3.Object, err error) {\n\tbucket, err := getBucketByName(bucketName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbucketPath := bucket.Path\n\n\tfp := path.Join(bucketPath, objectName)\n\tfmeta, _ := op.GetNearestMeta(fp)\n\tnode, err := fs.Get(context.WithValue(ctx, conf.MetaKey, fmeta), fp, &fs.GetArgs{})\n\tif err != nil {\n\t\treturn nil, gofakes3.KeyNotFound(objectName)\n\t}\n\n\tif node.IsDir() {\n\t\treturn nil, gofakes3.KeyNotFound(objectName)\n\t}\n\n\tlink, file, err := fs.Link(ctx, fp, model.LinkArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif s3Obj == nil {\n\t\t\t_ = link.Close()\n\t\t}\n\t}()\n\n\tsize := link.ContentLength\n\tif size <= 0 {\n\t\tsize = file.GetSize()\n\t}\n\trnge, err := rangeRequest.Range(size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trrf, err := stream.GetRangeReaderFromLink(size, link)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"the remote storage driver need to be enhanced to support s3\")\n\t}\n\n\tvar rd io.Reader\n\tif rnge != nil {\n\t\trd, err = rrf.RangeRead(ctx, http_range.Range(*rnge))\n\t} else {\n\t\trd, err = rrf.RangeRead(ctx, http_range.Range{Length: -1})\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmeta := map[string]string{\n\t\t\"Last-Modified\":       node.ModTime().Format(timeFormat),\n\t\t\"Content-Disposition\": utils.GenerateContentDisposition(file.GetName()),\n\t\t\"Content-Type\":        utils.GetMimeType(fp),\n\t}\n\n\tif val, ok := b.meta.Load(fp); ok {\n\t\tmetaMap := val.(map[string]string)\n\t\tfor k, v := range metaMap {\n\t\t\tmeta[k] = v\n\t\t}\n\t}\n\n\treturn &gofakes3.Object{\n\t\t// Name: gofakes3.URLEncode(objectName),\n\t\tName: objectName,\n\t\t// Hash:     \"\",\n\t\tMetadata: meta,\n\t\tSize:     size,\n\t\tRange:    rnge,\n\t\tContents: utils.ReadCloser{Reader: rd, Closer: link},\n\t}, nil\n}\n\n// TouchObject creates or updates meta on specified object.\nfunc (b *s3Backend) TouchObject(ctx context.Context, fp string, meta map[string]string) (result gofakes3.PutObjectResult, err error) {\n\t//TODO: implement\n\treturn result, gofakes3.ErrNotImplemented\n}\n\n// PutObject creates or overwrites the object with the given name.\nfunc (b *s3Backend) PutObject(\n\tctx context.Context, bucketName, objectName string,\n\tmeta map[string]string,\n\tinput io.Reader, size int64,\n) (result gofakes3.PutObjectResult, err error) {\n\tbucket, err := getBucketByName(bucketName)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\tbucketPath := bucket.Path\n\n\tisDir := strings.HasSuffix(objectName, \"/\")\n\tlog.Debugf(\"isDir: %v\", isDir)\n\n\tfp := path.Join(bucketPath, objectName)\n\tlog.Debugf(\"fp: %s, bucketPath: %s, objectName: %s\", fp, bucketPath, objectName)\n\n\tvar reqPath string\n\tif isDir {\n\t\treqPath = fp + \"/\"\n\t} else {\n\t\treqPath = path.Dir(fp)\n\t}\n\tlog.Debugf(\"reqPath: %s\", reqPath)\n\tfmeta, _ := op.GetNearestMeta(fp)\n\tctx = context.WithValue(ctx, conf.MetaKey, fmeta)\n\n\t_, err = fs.Get(ctx, reqPath, &fs.GetArgs{})\n\tif err != nil {\n\t\tif errs.IsObjectNotFound(err) && strings.Contains(objectName, \"/\") {\n\t\t\tlog.Debugf(\"reqPath: %s not found and objectName contains /, need to makeDir\", reqPath)\n\t\t\terr = fs.MakeDir(ctx, reqPath)\n\t\t\tif err != nil {\n\t\t\t\treturn result, errors.WithMessagef(err, \"failed to makeDir, reqPath: %s\", reqPath)\n\t\t\t}\n\t\t} else {\n\t\t\treturn result, gofakes3.KeyNotFound(objectName)\n\t\t}\n\t}\n\n\tif isDir {\n\t\treturn result, nil\n\t}\n\n\tvar ti time.Time\n\n\tif val, ok := meta[\"X-Amz-Meta-Mtime\"]; ok {\n\t\tti, _ = swift.FloatStringToTime(val)\n\t}\n\n\tif val, ok := meta[\"mtime\"]; ok {\n\t\tti, _ = swift.FloatStringToTime(val)\n\t}\n\n\t// If Modified is not set, use current time\n\tif ti.IsZero() {\n\t\tti = time.Now()\n\t}\n\n\tobj := model.Object{\n\t\tName:     path.Base(fp),\n\t\tSize:     size,\n\t\tModified: ti,\n\t\tCtime:    time.Now(),\n\t}\n\t// Check if system file should be ignored\n\tif setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(obj.Name) {\n\t\treturn result, errs.IgnoredSystemFile\n\t}\n\tstream := &stream.FileStream{\n\t\tObj:      &obj,\n\t\tReader:   input,\n\t\tMimetype: meta[\"Content-Type\"],\n\t}\n\n\terr = fs.PutDirectly(ctx, reqPath, stream)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\t// if err := stream.Close(); err != nil {\n\t// \t// remove file when close error occurred (FsPutErr)\n\t// \t_ = fs.Remove(ctx, fp)\n\t// \treturn result, err\n\t// }\n\n\tb.meta.Store(fp, meta)\n\n\treturn result, nil\n}\n\n// DeleteMulti deletes multiple objects in a single request.\nfunc (b *s3Backend) DeleteMulti(ctx context.Context, bucketName string, objects ...string) (result gofakes3.MultiDeleteResult, rerr error) {\n\tfor _, object := range objects {\n\t\tif err := b.deleteObject(ctx, bucketName, object); err != nil {\n\t\t\tlog.Errorf(\"delete object failed: %v\", err)\n\t\t\tresult.Error = append(result.Error, gofakes3.ErrorResult{\n\t\t\t\tCode:    gofakes3.ErrInternal,\n\t\t\t\tMessage: gofakes3.ErrInternal.Message(),\n\t\t\t\tKey:     object,\n\t\t\t})\n\t\t} else {\n\t\t\tresult.Deleted = append(result.Deleted, gofakes3.ObjectID{\n\t\t\t\tKey: object,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// DeleteObject deletes the object with the given name.\nfunc (b *s3Backend) DeleteObject(ctx context.Context, bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {\n\treturn result, b.deleteObject(ctx, bucketName, objectName)\n}\n\n// deleteObject deletes the object from the filesystem.\nfunc (b *s3Backend) deleteObject(ctx context.Context, bucketName, objectName string) error {\n\tbucket, err := getBucketByName(bucketName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbucketPath := bucket.Path\n\n\tfp := path.Join(bucketPath, objectName)\n\tfmeta, _ := op.GetNearestMeta(fp)\n\t// S3 does not report an error when attemping to delete a key that does not exist, so\n\t// we need to skip IsNotExist errors.\n\tif _, err := fs.Get(context.WithValue(ctx, conf.MetaKey, fmeta), fp, &fs.GetArgs{}); err != nil && !errs.IsObjectNotFound(err) {\n\t\treturn err\n\t}\n\n\tfs.Remove(ctx, fp)\n\treturn nil\n}\n\n// CreateBucket creates a new bucket.\nfunc (b *s3Backend) CreateBucket(ctx context.Context, name string) error {\n\treturn gofakes3.ErrNotImplemented\n}\n\n// DeleteBucket deletes the bucket with the given name.\nfunc (b *s3Backend) DeleteBucket(ctx context.Context, name string) error {\n\treturn gofakes3.ErrNotImplemented\n}\n\n// BucketExists checks if the bucket exists.\nfunc (b *s3Backend) BucketExists(ctx context.Context, name string) (exists bool, err error) {\n\tbuckets, err := getAndParseBuckets()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, b := range buckets {\n\t\tif b.Name == name {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// CopyObject copy specified object from srcKey to dstKey.\nfunc (b *s3Backend) CopyObject(ctx context.Context, srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {\n\tif srcBucket == dstBucket && srcKey == dstKey {\n\t\t//TODO: update meta\n\t\treturn result, nil\n\t}\n\n\tsrcB, err := getBucketByName(srcBucket)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\tsrcBucketPath := srcB.Path\n\n\tsrcFp := path.Join(srcBucketPath, srcKey)\n\tfmeta, _ := op.GetNearestMeta(srcFp)\n\tsrcNode, err := fs.Get(context.WithValue(ctx, conf.MetaKey, fmeta), srcFp, &fs.GetArgs{})\n\n\tc, err := b.GetObject(ctx, srcBucket, srcKey, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer func() {\n\t\t_ = c.Contents.Close()\n\t}()\n\n\tfor k, v := range c.Metadata {\n\t\tif _, found := meta[k]; !found && k != \"X-Amz-Acl\" {\n\t\t\tmeta[k] = v\n\t\t}\n\t}\n\tif _, ok := meta[\"mtime\"]; !ok {\n\t\tmeta[\"mtime\"] = swift.TimeToFloatString(srcNode.ModTime())\n\t}\n\n\t_, err = b.PutObject(ctx, dstBucket, dstKey, meta, c.Contents, c.Size)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn gofakes3.CopyObjectResult{\n\t\tETag:         `\"` + hex.EncodeToString(c.Hash) + `\"`,\n\t\tLastModified: gofakes3.NewContentTime(srcNode.ModTime()),\n\t}, nil\n}\n"
  },
  {
    "path": "server/s3/ioutils.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport \"io\"\n\ntype noOpReadCloser struct{}\n\ntype readerWithCloser struct {\n\tio.Reader\n\tcloser func() error\n}\n\nvar _ io.ReadCloser = &readerWithCloser{}\n\nfunc (d noOpReadCloser) Read(b []byte) (n int, err error) {\n\treturn 0, io.EOF\n}\n\nfunc (d noOpReadCloser) Close() error {\n\treturn nil\n}\n\nfunc limitReadCloser(rdr io.Reader, closer func() error, sz int64) io.ReadCloser {\n\treturn &readerWithCloser{\n\t\tReader: io.LimitReader(rdr, sz),\n\t\tcloser: closer,\n\t}\n}\n\nfunc (rwc *readerWithCloser) Close() error {\n\tif rwc.closer != nil {\n\t\treturn rwc.closer()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/s3/list.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/itsHenry35/gofakes3\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (b *s3Backend) entryListR(bucket, fdPath, name string, addPrefix bool, response *gofakes3.ObjectList) error {\n\tfp := path.Join(bucket, fdPath)\n\n\tdirEntries, err := getDirEntries(fp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// workaround as s3 can't have empty files in directories, useful in deletions\n\tif len(dirEntries) == 0 {\n\t\titem := &gofakes3.Content{\n\t\t\t// Key:          gofakes3.URLEncode(path.Join(fdPath, emptyObjectName)),\n\t\t\tKey:          path.Join(fdPath, emptyObjectName),\n\t\t\tLastModified: gofakes3.NewContentTime(time.Now()),\n\t\t\tETag:         getFileHash(nil), // No entry, so no hash\n\t\t\tSize:         0,\n\t\t\tStorageClass: gofakes3.StorageStandard,\n\t\t}\n\t\tresponse.Add(item)\n\t\tlog.Debugf(\"Adding empty object %s to response\", item.Key)\n\t\treturn nil\n\t}\n\n\tfor _, entry := range dirEntries {\n\t\tobject := entry.GetName()\n\n\t\t// workround for control-chars detect\n\t\tobjectPath := path.Join(fdPath, object)\n\n\t\tif !strings.HasPrefix(object, name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif entry.IsDir() {\n\t\t\tif addPrefix {\n\t\t\t\t// response.AddPrefix(gofakes3.URLEncode(objectPath))\n\t\t\t\tresponse.AddPrefix(objectPath)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr := b.entryListR(bucket, path.Join(fdPath, object), \"\", false, response)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\titem := &gofakes3.Content{\n\t\t\t\t// Key:          gofakes3.URLEncode(objectPath),\n\t\t\t\tKey:          objectPath,\n\t\t\t\tLastModified: gofakes3.NewContentTime(entry.ModTime()),\n\t\t\t\tETag:         getFileHash(entry),\n\t\t\t\tSize:         entry.GetSize(),\n\t\t\t\tStorageClass: gofakes3.StorageStandard,\n\t\t\t}\n\t\t\tresponse.Add(item)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/s3/logger.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/itsHenry35/gofakes3\"\n)\n\n// logger output formatted message\ntype logger struct{}\n\n// print log message\nfunc (l logger) Print(level gofakes3.LogLevel, v ...interface{}) {\n\tswitch level {\n\tdefault:\n\t\tfallthrough\n\tcase gofakes3.LogErr:\n\t\tutils.Log.Errorf(\"serve s3: %s\", fmt.Sprintln(v...))\n\tcase gofakes3.LogWarn:\n\t\tutils.Log.Infof(\"serve s3: %s\", fmt.Sprintln(v...))\n\tcase gofakes3.LogInfo:\n\t\tutils.Log.Debugf(\"serve s3: %s\", fmt.Sprintln(v...))\n\t}\n}\n"
  },
  {
    "path": "server/s3/pager.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"sort\"\n\n\t\"github.com/itsHenry35/gofakes3\"\n)\n\n// pager splits the object list into smulitply pages.\nfunc (db *s3Backend) pager(list *gofakes3.ObjectList, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {\n\t// sort by alphabet\n\tsort.Slice(list.CommonPrefixes, func(i, j int) bool {\n\t\treturn list.CommonPrefixes[i].Prefix < list.CommonPrefixes[j].Prefix\n\t})\n\t// sort by modtime\n\tsort.Slice(list.Contents, func(i, j int) bool {\n\t\treturn list.Contents[i].LastModified.Before(list.Contents[j].LastModified.Time)\n\t})\n\ttokens := page.MaxKeys\n\tif tokens == 0 {\n\t\ttokens = 1000\n\t}\n\tif page.HasMarker {\n\t\tfor i, obj := range list.Contents {\n\t\t\tif obj.Key == page.Marker {\n\t\t\t\tlist.Contents = list.Contents[i+1:]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor i, obj := range list.CommonPrefixes {\n\t\t\tif obj.Prefix == page.Marker {\n\t\t\t\tlist.CommonPrefixes = list.CommonPrefixes[i+1:]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tresponse := gofakes3.NewObjectList()\n\tfor _, obj := range list.CommonPrefixes {\n\t\tif tokens <= 0 {\n\t\t\tbreak\n\t\t}\n\t\tresponse.AddPrefix(obj.Prefix)\n\t\ttokens--\n\t}\n\n\tfor _, obj := range list.Contents {\n\t\tif tokens <= 0 {\n\t\t\tbreak\n\t\t}\n\t\tresponse.Add(obj)\n\t\ttokens--\n\t}\n\n\tif len(list.CommonPrefixes)+len(list.Contents) > int(page.MaxKeys) {\n\t\tresponse.IsTruncated = true\n\t\tif len(response.Contents) > 0 {\n\t\t\tresponse.NextMarker = response.Contents[len(response.Contents)-1].Key\n\t\t} else {\n\t\t\tresponse.NextMarker = response.CommonPrefixes[len(response.CommonPrefixes)-1].Prefix\n\t\t}\n\t}\n\n\treturn response, nil\n}\n"
  },
  {
    "path": "server/s3/server.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"net/http\"\n\n\t\"github.com/itsHenry35/gofakes3\"\n)\n\n// Make a new S3 Server to serve the remote\nfunc NewServer(ctx context.Context) (h http.Handler, err error) {\n\tvar newLogger logger\n\tfaker := gofakes3.New(\n\t\tnewBackend(),\n\t\t// gofakes3.WithHostBucket(!opt.pathBucketMode),\n\t\tgofakes3.WithLogger(newLogger),\n\t\tgofakes3.WithRequestID(rand.Uint64()),\n\t\tgofakes3.WithoutVersioning(),\n\t\tgofakes3.WithV4Auth(authlistResolver()),\n\t\tgofakes3.WithIntegrityCheck(true), // Check Content-MD5 if supplied\n\t)\n\n\treturn faker.Server(), nil\n}\n"
  },
  {
    "path": "server/s3/utils.go",
    "content": "// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3\n// Package s3 implements a fake s3 server for openlist\npackage s3\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/itsHenry35/gofakes3\"\n)\n\ntype Bucket struct {\n\tName string `json:\"name\"`\n\tPath string `json:\"path\"`\n}\n\nconst emptyObjectName = \"ThisIsAnEmptyFolderInTheS3Bucket\"\n\nfunc getAndParseBuckets() ([]Bucket, error) {\n\tvar res []Bucket\n\terr := json.Unmarshal([]byte(setting.GetStr(conf.S3Buckets)), &res)\n\treturn res, err\n}\n\nfunc getBucketByName(name string) (Bucket, error) {\n\tbuckets, err := getAndParseBuckets()\n\tif err != nil {\n\t\treturn Bucket{}, err\n\t}\n\tfor _, b := range buckets {\n\t\tif b.Name == name {\n\t\t\treturn b, nil\n\t\t}\n\t}\n\treturn Bucket{}, gofakes3.BucketNotFound(name)\n}\n\nfunc getDirEntries(path string) ([]model.Obj, error) {\n\tctx := context.Background()\n\tmeta, _ := op.GetNearestMeta(path)\n\tfi, err := fs.Get(context.WithValue(ctx, conf.MetaKey, meta), path, &fs.GetArgs{})\n\tif errs.IsNotFoundError(err) {\n\t\treturn nil, gofakes3.ErrNoSuchKey\n\t} else if err != nil {\n\t\treturn nil, gofakes3.ErrNoSuchKey\n\t}\n\n\tif !fi.IsDir() {\n\t\treturn nil, gofakes3.ErrNoSuchKey\n\t}\n\n\tdirEntries, err := fs.List(context.WithValue(ctx, conf.MetaKey, meta), path, &fs.ListArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dirEntries, nil\n}\n\n// func getFileHashByte(node interface{}) []byte {\n// \tb, err := hex.DecodeString(getFileHash(node))\n// \tif err != nil {\n// \t\treturn nil\n// \t}\n// \treturn b\n// }\n\nfunc getFileHash(node interface{}) string {\n\t// var o fs.Object\n\n\t// switch b := node.(type) {\n\t// case vfs.Node:\n\t// \tfsObj, ok := b.DirEntry().(fs.Object)\n\t// \tif !ok {\n\t// \t\tfs.Debugf(\"serve s3\", \"File uploading - reading hash from VFS cache\")\n\t// \t\tin, err := b.Open(os.O_RDONLY)\n\t// \t\tif err != nil {\n\t// \t\t\treturn \"\"\n\t// \t\t}\n\t// \t\tdefer func() {\n\t// \t\t\t_ = in.Close()\n\t// \t\t}()\n\t// \t\th, err := hash.NewMultiHasherTypes(hash.NewHashSet(hash.MD5))\n\t// \t\tif err != nil {\n\t// \t\t\treturn \"\"\n\t// \t\t}\n\t// \t\t_, err = io.Copy(h, in)\n\t// \t\tif err != nil {\n\t// \t\t\treturn \"\"\n\t// \t\t}\n\t// \t\treturn h.Sums()[hash.MD5]\n\t// \t}\n\t// \to = fsObj\n\t// case fs.Object:\n\t// \to = b\n\t// }\n\n\t// hash, err := o.Hash(context.Background(), hash.MD5)\n\t// if err != nil {\n\t// \treturn \"\"\n\t// }\n\t// return hash\n\treturn \"\"\n}\n\nfunc prefixParser(p *gofakes3.Prefix) (path, remaining string) {\n\tidx := strings.LastIndexByte(p.Prefix, '/')\n\tif idx < 0 {\n\t\treturn \"\", p.Prefix\n\t}\n\treturn p.Prefix[:idx], p.Prefix[idx+1:]\n}\n\n// // FIXME this could be implemented by VFS.MkdirAll()\n// func mkdirRecursive(path string, VFS *vfs.VFS) error {\n// \tpath = strings.Trim(path, \"/\")\n// \tdirs := strings.Split(path, \"/\")\n// \tdir := \"\"\n// \tfor _, d := range dirs {\n// \t\tdir += \"/\" + d\n// \t\tif _, err := VFS.Stat(dir); err != nil {\n// \t\t\terr := VFS.Mkdir(dir, 0777)\n// \t\t\tif err != nil {\n// \t\t\t\treturn err\n// \t\t\t}\n// \t\t}\n// \t}\n// \treturn nil\n// }\n\n// func rmdirRecursive(p string, VFS *vfs.VFS) {\n// \tdir := path.Dir(p)\n// \tif !strings.ContainsAny(dir, \"/\\\\\") {\n// \t\t// might be bucket(root)\n// \t\treturn\n// \t}\n// \tif _, err := VFS.Stat(dir); err == nil {\n// \t\terr := VFS.Remove(dir)\n// \t\tif err != nil {\n// \t\t\treturn\n// \t\t}\n// \t\trmdirRecursive(dir, VFS)\n// \t}\n// }\n\nfunc authlistResolver() map[string]string {\n\ts3accesskeyid := setting.GetStr(conf.S3AccessKeyId)\n\ts3secretaccesskey := setting.GetStr(conf.S3SecretAccessKey)\n\tif s3accesskeyid == \"\" && s3secretaccesskey == \"\" {\n\t\treturn nil\n\t}\n\tauthList := make(map[string]string)\n\tauthList[s3accesskeyid] = s3secretaccesskey\n\treturn authList\n}\n"
  },
  {
    "path": "server/s3.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/s3\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc S3(g *gin.RouterGroup) {\n\tif !conf.Conf.S3.Enable {\n\t\tg.Any(\"/*path\", func(c *gin.Context) {\n\t\t\tcommon.ErrorStrResp(c, \"S3 server is not enabled\", 403)\n\t\t})\n\t\treturn\n\t}\n\tif conf.Conf.S3.Port != -1 {\n\t\tg.Any(\"/*path\", func(c *gin.Context) {\n\t\t\tcommon.ErrorStrResp(c, \"S3 server bound to single port\", 403)\n\t\t})\n\t\treturn\n\t}\n\th, _ := s3.NewServer(context.Background())\n\n\tg.Any(\"/*path\", func(c *gin.Context) {\n\t\tadjustedPath := strings.TrimPrefix(c.Request.URL.Path, path.Join(conf.URL.Path, \"/s3\"))\n\t\tc.Request.URL.Path = adjustedPath\n\t\tgin.WrapH(h)(c)\n\t})\n}\n\nfunc S3Server(g *gin.RouterGroup) {\n\th, _ := s3.NewServer(context.Background())\n\tg.Any(\"/*path\", gin.WrapH(h))\n}\n"
  },
  {
    "path": "server/sftp/const.go",
    "content": "package sftp\n\n// From leffss/sftpd\nconst (\n\tSSH_FXF_READ   = 0x00000001\n\tSSH_FXF_WRITE  = 0x00000002\n\tSSH_FXF_APPEND = 0x00000004\n\tSSH_FXF_CREAT  = 0x00000008\n\tSSH_FXF_TRUNC  = 0x00000010\n\tSSH_FXF_EXCL   = 0x00000020\n)\n"
  },
  {
    "path": "server/sftp/hostkey.go",
    "content": "package sftp\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/cmd/flags\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nvar SSHSigners []ssh.Signer\n\nfunc InitHostKey() {\n\tif SSHSigners != nil {\n\t\treturn\n\t}\n\tsshPath := filepath.Join(flags.DataDir, \"ssh\")\n\tif !utils.Exists(sshPath) {\n\t\terr := utils.CreateNestedDirectory(sshPath)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to create ssh directory: %+v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\tSSHSigners = make([]ssh.Signer, 0, 4)\n\tif rsaKey, ok := LoadOrGenerateRSAHostKey(sshPath); ok {\n\t\tSSHSigners = append(SSHSigners, rsaKey)\n\t}\n\t// TODO Add keys for other encryption algorithms\n}\n\nfunc LoadOrGenerateRSAHostKey(parentDir string) (ssh.Signer, bool) {\n\tprivateKeyPath := filepath.Join(parentDir, \"ssh_host_rsa_key\")\n\tpublicKeyPath := filepath.Join(parentDir, \"ssh_host_rsa_key.pub\")\n\tprivateKeyBytes, err := os.ReadFile(privateKeyPath)\n\tif err == nil {\n\t\tvar privateKey *rsa.PrivateKey\n\t\tprivateKey, err = rsaDecodePrivateKey(privateKeyBytes)\n\t\tif err == nil {\n\t\t\tvar ret ssh.Signer\n\t\t\tret, err = ssh.NewSignerFromKey(privateKey)\n\t\t\tif err == nil {\n\t\t\t\treturn ret, true\n\t\t\t}\n\t\t}\n\t}\n\t_ = os.Remove(privateKeyPath)\n\t_ = os.Remove(publicKeyPath)\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to generate RSA private key: %+v\", err)\n\t\treturn nil, false\n\t}\n\tpublicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to generate RSA public key: %+v\", err)\n\t\treturn nil, false\n\t}\n\tret, err := ssh.NewSignerFromKey(privateKey)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to generate RSA signer: %+v\", err)\n\t\treturn nil, false\n\t}\n\tprivateBytes := rsaEncodePrivateKey(privateKey)\n\tpublicBytes := ssh.MarshalAuthorizedKey(publicKey)\n\terr = os.WriteFile(privateKeyPath, privateBytes, 0600)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to write RSA private key to file: %+v\", err)\n\t\treturn nil, false\n\t}\n\terr = os.WriteFile(publicKeyPath, publicBytes, 0644)\n\tif err != nil {\n\t\t_ = os.Remove(privateKeyPath)\n\t\tutils.Log.Errorf(\"failed to write RSA public key to file: %+v\", err)\n\t\treturn nil, false\n\t}\n\treturn ret, true\n}\n\nfunc rsaEncodePrivateKey(privateKey *rsa.PrivateKey) []byte {\n\tprivateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)\n\tprivateBlock := &pem.Block{\n\t\tType:    \"RSA PRIVATE KEY\",\n\t\tHeaders: nil,\n\t\tBytes:   privateKeyBytes,\n\t}\n\treturn pem.EncodeToMemory(privateBlock)\n}\n\nfunc rsaDecodePrivateKey(bytes []byte) (*rsa.PrivateKey, error) {\n\tblock, _ := pem.Decode(bytes)\n\tif block == nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse PEM block containing the key\")\n\t}\n\tprivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn privateKey, nil\n}\n"
  },
  {
    "path": "server/sftp/sftp.go",
    "content": "package sftp\n\nimport (\n\t\"os\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/ftp\"\n\t\"github.com/OpenListTeam/sftpd-openlist\"\n)\n\ntype DriverAdapter struct {\n\tFtpDriver *ftp.AferoAdapter\n}\n\nfunc (s *DriverAdapter) OpenFile(_ string, _ uint32, _ *sftpd.Attr) (sftpd.File, error) {\n\t// See also GetHandle\n\treturn nil, errs.NotImplement\n}\n\nfunc (s *DriverAdapter) OpenDir(_ string) (sftpd.Dir, error) {\n\t// See also GetHandle\n\treturn nil, errs.NotImplement\n}\n\nfunc (s *DriverAdapter) Remove(name string) error {\n\treturn s.FtpDriver.Remove(name)\n}\n\nfunc (s *DriverAdapter) Rename(old, new string, _ uint32) error {\n\treturn s.FtpDriver.Rename(old, new)\n}\n\nfunc (s *DriverAdapter) Mkdir(name string, attr *sftpd.Attr) error {\n\treturn s.FtpDriver.Mkdir(name, attr.Mode)\n}\n\nfunc (s *DriverAdapter) Rmdir(name string) error {\n\treturn s.Remove(name)\n}\n\nfunc (s *DriverAdapter) Stat(name string, _ bool) (*sftpd.Attr, error) {\n\tstat, err := s.FtpDriver.Stat(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fileInfoToSftpAttr(stat), nil\n}\n\nfunc (s *DriverAdapter) SetStat(_ string, _ *sftpd.Attr) error {\n\treturn errs.NotSupport\n}\n\nfunc (s *DriverAdapter) ReadLink(_ string) (string, error) {\n\treturn \"\", errs.NotSupport\n}\n\nfunc (s *DriverAdapter) CreateLink(_, _ string, _ uint32) error {\n\treturn errs.NotSupport\n}\n\nfunc (s *DriverAdapter) RealPath(path string) (string, error) {\n\treturn utils.FixAndCleanPath(path), nil\n}\n\nfunc (s *DriverAdapter) GetHandle(name string, flags uint32, _ *sftpd.Attr, offset uint64) (sftpd.FileTransfer, error) {\n\treturn s.FtpDriver.GetHandle(name, sftpFlagToOpenMode(flags), int64(offset))\n}\n\nfunc (s *DriverAdapter) ReadDir(name string) ([]sftpd.NamedAttr, error) {\n\tdir, err := s.FtpDriver.ReadDir(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tret := make([]sftpd.NamedAttr, len(dir))\n\tfor i, d := range dir {\n\t\tret[i] = *fileInfoToSftpNamedAttr(d)\n\t}\n\treturn ret, nil\n}\n\n// From leffss/sftpd\nfunc sftpFlagToOpenMode(flags uint32) int {\n\tmode := 0\n\tif (flags & SSH_FXF_READ) != 0 {\n\t\tmode |= os.O_RDONLY\n\t}\n\tif (flags & SSH_FXF_WRITE) != 0 {\n\t\tmode |= os.O_WRONLY\n\t}\n\tif (flags & SSH_FXF_APPEND) != 0 {\n\t\tmode |= os.O_APPEND\n\t}\n\tif (flags & SSH_FXF_CREAT) != 0 {\n\t\tmode |= os.O_CREATE\n\t}\n\tif (flags & SSH_FXF_TRUNC) != 0 {\n\t\tmode |= os.O_TRUNC\n\t}\n\tif (flags & SSH_FXF_EXCL) != 0 {\n\t\tmode |= os.O_EXCL\n\t}\n\treturn mode\n}\n\nfunc fileInfoToSftpAttr(stat os.FileInfo) *sftpd.Attr {\n\tret := &sftpd.Attr{}\n\tret.Flags |= sftpd.ATTR_SIZE\n\tret.Size = uint64(stat.Size())\n\tret.Flags |= sftpd.ATTR_MODE\n\tret.Mode = stat.Mode()\n\tret.Flags |= sftpd.ATTR_TIME\n\tret.ATime = stat.Sys().(model.Obj).CreateTime()\n\tret.MTime = stat.ModTime()\n\treturn ret\n}\n\nfunc fileInfoToSftpNamedAttr(stat os.FileInfo) *sftpd.NamedAttr {\n\treturn &sftpd.NamedAttr{\n\t\tName: stat.Name(),\n\t\tAttr: *fileInfoToSftpAttr(stat),\n\t}\n}\n"
  },
  {
    "path": "server/sftp.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/ftp\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/sftp\"\n\t\"github.com/OpenListTeam/sftpd-openlist\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype SftpDriver struct {\n\tproxyHeader http.Header\n\tconfig      *sftpd.Config\n}\n\nfunc NewSftpDriver() (*SftpDriver, error) {\n\tftp.InitStage()\n\tsftp.InitHostKey()\n\treturn &SftpDriver{\n\t\tproxyHeader: http.Header{\n\t\t\t\"User-Agent\": {base.UserAgent},\n\t\t},\n\t}, nil\n}\n\nfunc (d *SftpDriver) GetConfig() *sftpd.Config {\n\tif d.config != nil {\n\t\treturn d.config\n\t}\n\tvar pwdAuth func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) = nil\n\tif !setting.GetBool(conf.SFTPDisablePasswordLogin) {\n\t\tpwdAuth = d.PasswordAuth\n\t}\n\tserverConfig := ssh.ServerConfig{\n\t\tNoClientAuth:         true,\n\t\tNoClientAuthCallback: d.NoClientAuth,\n\t\tPasswordCallback:     pwdAuth,\n\t\tPublicKeyCallback:    d.PublicKeyAuth,\n\t\tAuthLogCallback:      d.AuthLogCallback,\n\t\tBannerCallback:       d.GetBanner,\n\t}\n\tfor _, k := range sftp.SSHSigners {\n\t\tserverConfig.AddHostKey(k)\n\t}\n\td.config = &sftpd.Config{\n\t\tServerConfig: serverConfig,\n\t\tHostPort:     conf.Conf.SFTP.Listen,\n\t\tErrorLogFunc: utils.Log.Error,\n\t\t// DebugLogFunc: utils.Log.Debugf,\n\t}\n\treturn d.config\n}\n\nfunc (d *SftpDriver) GetFileSystem(sc *ssh.ServerConn) (sftpd.FileSystem, error) {\n\tuserObj, err := op.GetUserByName(sc.User())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tctx := context.Background()\n\tctx = context.WithValue(ctx, conf.UserKey, userObj)\n\tctx = context.WithValue(ctx, conf.MetaPassKey, \"\")\n\tctx = context.WithValue(ctx, conf.ClientIPKey, sc.RemoteAddr().String())\n\tctx = context.WithValue(ctx, conf.ProxyHeaderKey, d.proxyHeader)\n\treturn &sftp.DriverAdapter{FtpDriver: ftp.NewAferoAdapter(ctx)}, nil\n}\n\nfunc (d *SftpDriver) Close() {\n}\n\nfunc (d *SftpDriver) NoClientAuth(conn ssh.ConnMetadata) (*ssh.Permissions, error) {\n\tif conn.User() != \"guest\" {\n\t\treturn nil, errors.New(\"only guest is allowed to login without authorization\")\n\t}\n\tguest, err := op.GetGuest()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif guest.Disabled || !guest.CanFTPAccess() {\n\t\treturn nil, errors.New(\"user is not allowed to access via SFTP\")\n\t}\n\treturn nil, nil\n}\n\nfunc (d *SftpDriver) PasswordAuth(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {\n\tip := conn.RemoteAddr().String()\n\tcount, ok := model.LoginCache.Get(ip)\n\tif ok && count >= model.DefaultMaxAuthRetries {\n\t\tmodel.LoginCache.Expire(ip, model.DefaultLockDuration)\n\t\treturn nil, errors.New(\"Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.\")\n\t}\n\tpass := string(password)\n\tuserObj, err := op.GetUserByName(conn.User())\n\tif err == nil {\n\t\terr = userObj.ValidateRawPassword(pass)\n\t\tif err != nil && setting.GetBool(conf.LdapLoginEnabled) && userObj.AllowLdap {\n\t\t\terr = common.HandleLdapLogin(conn.User(), pass)\n\t\t}\n\t} else if setting.GetBool(conf.LdapLoginEnabled) && model.CanFTPAccess(int32(setting.GetInt(conf.LdapDefaultPermission, 0))) {\n\t\tuserObj, err = tryLdapLoginAndRegister(conn.User(), pass)\n\t}\n\tif err != nil {\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\treturn nil, err\n\t}\n\tif userObj.Disabled || !userObj.CanFTPAccess() {\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\treturn nil, errors.New(\"user is not allowed to access via SFTP\")\n\t}\n\tmodel.LoginCache.Del(ip)\n\treturn nil, nil\n}\n\nfunc (d *SftpDriver) PublicKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {\n\tuserObj, err := op.GetUserByName(conn.User())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userObj.Disabled || !userObj.CanFTPAccess() {\n\t\treturn nil, errors.New(\"user is not allowed to access via SFTP\")\n\t}\n\tkeys, _, err := op.GetSSHPublicKeyByUserId(userObj.ID, 1, -1)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmarshal := string(key.Marshal())\n\tfor _, sk := range keys {\n\t\tif marshal != sk.KeyStr {\n\t\t\tpubKey, _, _, _, e := ssh.ParseAuthorizedKey([]byte(sk.KeyStr))\n\t\t\tif e != nil || marshal != string(pubKey.Marshal()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tsk.LastUsedTime = time.Now()\n\t\t_ = op.UpdateSSHPublicKey(&sk)\n\t\treturn nil, nil\n\t}\n\treturn nil, errors.New(\"public key refused\")\n}\n\nfunc (d *SftpDriver) AuthLogCallback(conn ssh.ConnMetadata, method string, err error) {\n\tip := conn.RemoteAddr().String()\n\tif err == nil {\n\t\tutils.Log.Infof(\"[SFTP] %s(%s) logged in via %s\", conn.User(), ip, method)\n\t} else if method != \"none\" {\n\t\tutils.Log.Infof(\"[SFTP] %s(%s) tries logging in via %s but with error: %s\", conn.User(), ip, method, err)\n\t}\n}\n\nfunc (d *SftpDriver) GetBanner(_ ssh.ConnMetadata) string {\n\treturn setting.GetStr(conf.Announcement)\n}\n"
  },
  {
    "path": "server/static/config.go",
    "content": "package static\n\nimport (\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n)\n\ntype SiteConfig struct {\n\tBasePath string\n\tCdn      string\n}\n\nfunc getSiteConfig() SiteConfig {\n\tsiteConfig := SiteConfig{\n\t\tBasePath: conf.URL.Path,\n\t\tCdn:      strings.ReplaceAll(strings.TrimSuffix(conf.Conf.Cdn, \"/\"), \"$version\", strings.TrimPrefix(conf.WebVersion, \"v\")),\n\t}\n\tif siteConfig.BasePath != \"\" {\n\t\tsiteConfig.BasePath = utils.FixAndCleanPath(siteConfig.BasePath)\n\t\t// Keep consistent with frontend: trim trailing slash unless it's root\n\t\tif siteConfig.BasePath != \"/\" && strings.HasSuffix(siteConfig.BasePath, \"/\") {\n\t\t\tsiteConfig.BasePath = strings.TrimSuffix(siteConfig.BasePath, \"/\")\n\t\t}\n\t}\n\tif siteConfig.BasePath == \"\" {\n\t\tsiteConfig.BasePath = \"/\"\n\t}\n\tif siteConfig.Cdn == \"\" {\n\t\tsiteConfig.Cdn = strings.TrimSuffix(siteConfig.BasePath, \"/\")\n\t}\n\treturn siteConfig\n}\n"
  },
  {
    "path": "server/static/static.go",
    "content": "package static\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/drivers/base\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/public\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype ManifestIcon struct {\n\tSrc   string `json:\"src\"`\n\tSizes string `json:\"sizes\"`\n\tType  string `json:\"type\"`\n}\n\ntype Manifest struct {\n\tDisplay  string         `json:\"display\"`\n\tScope    string         `json:\"scope\"`\n\tStartURL string         `json:\"start_url\"`\n\tName     string         `json:\"name\"`\n\tIcons    []ManifestIcon `json:\"icons\"`\n}\n\nvar static fs.FS\n\nfunc initStatic() {\n\tutils.Log.Debug(\"Initializing static file system...\")\n\tif conf.Conf.DistDir == \"\" {\n\t\tdist, err := fs.Sub(public.Public, \"dist\")\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"failed to read dist dir: %v\", err)\n\t\t}\n\t\tstatic = dist\n\t\tutils.Log.Debug(\"Using embedded dist directory\")\n\t\treturn\n\t}\n\tstatic = os.DirFS(conf.Conf.DistDir)\n\tutils.Log.Infof(\"Using custom dist directory: %s\", conf.Conf.DistDir)\n}\n\nfunc replaceStrings(content string, replacements map[string]string) string {\n\tfor old, new := range replacements {\n\t\tcontent = strings.Replace(content, old, new, 1)\n\t}\n\treturn content\n}\n\nfunc initIndex(siteConfig SiteConfig) {\n\tutils.Log.Debug(\"Initializing index.html...\")\n\t// dist_dir is empty and cdn is not empty, and web_version is empty or beta or dev or rolling\n\tif conf.Conf.DistDir == \"\" && conf.Conf.Cdn != \"\" && (conf.WebVersion == \"\" || conf.WebVersion == \"beta\" || conf.WebVersion == \"dev\" || conf.WebVersion == \"rolling\") {\n\t\tutils.Log.Infof(\"Fetching index.html from CDN: %s/index.html...\", siteConfig.Cdn)\n\t\tresp, err := base.RestyClient.R().\n\t\t\tSetHeader(\"Accept\", \"text/html\").\n\t\t\tGet(fmt.Sprintf(\"%s/index.html\", siteConfig.Cdn))\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"failed to fetch index.html from CDN: %v\", err)\n\t\t}\n\t\tif resp.StatusCode() != http.StatusOK {\n\t\t\tutils.Log.Fatalf(\"failed to fetch index.html from CDN, status code: %d\", resp.StatusCode())\n\t\t}\n\t\tconf.RawIndexHtml = string(resp.Body())\n\t\tutils.Log.Info(\"Successfully fetched index.html from CDN\")\n\t} else {\n\t\tutils.Log.Debug(\"Reading index.html from static files system...\")\n\t\tindexFile, err := static.Open(\"index.html\")\n\t\tif err != nil {\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tutils.Log.Fatalf(\"index.html not exist, you may forget to put dist of frontend to public/dist\")\n\t\t\t}\n\t\t\tutils.Log.Fatalf(\"failed to read index.html: %v\", err)\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = indexFile.Close()\n\t\t}()\n\t\tindex, err := io.ReadAll(indexFile)\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"failed to read dist/index.html\")\n\t\t}\n\t\tconf.RawIndexHtml = string(index)\n\t\tutils.Log.Debug(\"Successfully read index.html from static files system\")\n\t}\n\tutils.Log.Debug(\"Replacing placeholders in index.html...\")\n\t// Construct the correct manifest path based on basePath\n\tmanifestPath := \"/manifest.json\"\n\tif siteConfig.BasePath != \"/\" {\n\t\tmanifestPath = siteConfig.BasePath + \"/manifest.json\"\n\t}\n\treplaceMap := map[string]string{\n\t\t\"cdn: undefined\":                    fmt.Sprintf(\"cdn: '%s'\", siteConfig.Cdn),\n\t\t\"base_path: undefined\":              fmt.Sprintf(\"base_path: '%s'\", siteConfig.BasePath),\n\t\t`href=\"/manifest.json\"`:             fmt.Sprintf(`href=\"%s\"`, manifestPath),\n\t}\n\tconf.RawIndexHtml = replaceStrings(conf.RawIndexHtml, replaceMap)\n\tUpdateIndex()\n}\n\nfunc UpdateIndex() {\n\tutils.Log.Debug(\"Updating index.html with settings...\")\n\tfavicon := setting.GetStr(conf.Favicon)\n\tlogo := strings.Split(setting.GetStr(conf.Logo), \"\\n\")[0]\n\ttitle := setting.GetStr(conf.SiteTitle)\n\tcustomizeHead := setting.GetStr(conf.CustomizeHead)\n\tcustomizeBody := setting.GetStr(conf.CustomizeBody)\n\tmainColor := setting.GetStr(conf.MainColor)\n\tutils.Log.Debug(\"Applying replacements for default pages...\")\n\treplaceMap1 := map[string]string{\n\t\t\"https://res.oplist.org/logo/logo.svg\": favicon,\n\t\t\"https://res.oplist.org/logo/logo.png\": logo,\n\t\t\"Loading...\":                           title,\n\t\t\"main_color: undefined\":                fmt.Sprintf(\"main_color: '%s'\", mainColor),\n\t}\n\tconf.ManageHtml = replaceStrings(conf.RawIndexHtml, replaceMap1)\n\tutils.Log.Debug(\"Applying replacements for manage pages...\")\n\treplaceMap2 := map[string]string{\n\t\t\"<!-- customize head -->\": customizeHead,\n\t\t\"<!-- customize body -->\": customizeBody,\n\t}\n\tconf.IndexHtml = replaceStrings(conf.ManageHtml, replaceMap2)\n\tutils.Log.Debug(\"Index.html update completed\")\n}\n\nfunc ManifestJSON(c *gin.Context) {\n\t// Get site configuration to ensure consistent base path handling\n\tsiteConfig := getSiteConfig()\n\t\n\t// Get site title from settings\n\tsiteTitle := setting.GetStr(conf.SiteTitle)\n\t\n\t// Get logo from settings, use the first line (light theme logo)\n\tlogoSetting := setting.GetStr(conf.Logo)\n\tlogoUrl := strings.Split(logoSetting, \"\\n\")[0]\n\n\t// Use base path from site config for consistency\n\tbasePath := siteConfig.BasePath\n\n\t// Determine scope and start_url\n\t// PWA scope and start_url should always point to our application's base path\n\t// regardless of whether static resources come from CDN or local server\n\tscope := basePath\n\tstartURL := basePath\n\n\tmanifest := Manifest{\n\t\tDisplay:  \"standalone\",\n\t\tScope:    scope,\n\t\tStartURL: startURL,\n\t\tName:     siteTitle,\n\t\tIcons: []ManifestIcon{\n\t\t\t{\n\t\t\t\tSrc:   logoUrl,\n\t\t\t\tSizes: \"512x512\",\n\t\t\t\tType:  \"image/png\",\n\t\t\t},\n\t\t},\n\t}\n\n\tc.Header(\"Content-Type\", \"application/json\")\n\tc.Header(\"Cache-Control\", \"public, max-age=3600\") // cache for 1 hour\n\t\n\tif err := json.NewEncoder(c.Writer).Encode(manifest); err != nil {\n\t\tutils.Log.Errorf(\"Failed to encode manifest.json: %v\", err)\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"Failed to generate manifest\"})\n\t\treturn\n\t}\n}\n\nfunc Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {\n\tutils.Log.Debug(\"Setting up static routes...\")\n\tsiteConfig := getSiteConfig()\n\tinitStatic()\n\tinitIndex(siteConfig)\n\tfolders := []string{\"assets\", \"images\", \"streamer\", \"static\"}\n\t\n\tif conf.Conf.Cdn == \"\" {\n\t\tutils.Log.Debug(\"Setting up static file serving...\")\n\t\tr.Use(func(c *gin.Context) {\n\t\t\tfor _, folder := range folders {\n\t\t\t\tif strings.HasPrefix(c.Request.RequestURI, fmt.Sprintf(\"/%s/\", folder)) {\n\t\t\t\t\tc.Header(\"Cache-Control\", \"public, max-age=15552000\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\tfor _, folder := range folders {\n\t\t\tsub, err := fs.Sub(static, folder)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Fatalf(\"can't find folder: %s\", folder)\n\t\t\t}\n\t\t\tutils.Log.Debugf(\"Setting up route for folder: %s\", folder)\n\t\t\tr.StaticFS(fmt.Sprintf(\"/%s/\", folder), http.FS(sub))\n\t\t}\n\t} else {\n\t\t// Ensure static file redirected to CDN\n\t\tfor _, folder := range folders {\n\t\t\tr.GET(fmt.Sprintf(\"/%s/*filepath\", folder), func(c *gin.Context) {\n\t\t\t\tfilepath := c.Param(\"filepath\")\n\t\t\t\tc.Redirect(http.StatusFound, fmt.Sprintf(\"%s/%s%s\", siteConfig.Cdn, folder, filepath))\n\t\t\t})\n\t\t}\n\t}\n\n\tutils.Log.Debug(\"Setting up catch-all route...\")\n\tnoRoute(func(c *gin.Context) {\n\t\tif c.Request.Method != \"GET\" && c.Request.Method != \"POST\" {\n\t\t\tc.Status(405)\n\t\t\treturn\n\t\t}\n\t\tc.Header(\"Content-Type\", \"text/html\")\n\t\tc.Status(200)\n\t\tif strings.HasPrefix(c.Request.URL.Path, \"/@manage\") {\n\t\t\t_, _ = c.Writer.WriteString(conf.ManageHtml)\n\t\t} else {\n\t\t\t_, _ = c.Writer.WriteString(conf.IndexHtml)\n\t\t}\n\t\tc.Writer.Flush()\n\t\tc.Writer.WriteHeaderNow()\n\t})\n}\n"
  },
  {
    "path": "server/utils.go",
    "content": "package server\n\nimport (\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\nfunc tryLdapLoginAndRegister(user, pass string) (*model.User, error) {\n\terr := common.HandleLdapLogin(user, pass)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn common.LdapRegister(user)\n}\n"
  },
  {
    "path": "server/webdav/buffered_response_writer.go",
    "content": "package webdav\n\nimport (\n\t\"net/http\"\n)\n\ntype bufferedResponseWriter struct {\n\tstatusCode int\n\tdata       []byte\n\theader     http.Header\n}\n\nfunc (w *bufferedResponseWriter) Header() http.Header {\n\tif w.header == nil {\n\t\tw.header = make(http.Header)\n\t}\n\treturn w.header\n}\n\nfunc (w *bufferedResponseWriter) Write(bytes []byte) (int, error) {\n\tw.data = append(w.data, bytes...)\n\treturn len(bytes), nil\n}\n\nfunc (w *bufferedResponseWriter) WriteHeader(statusCode int) {\n\tif w.statusCode == 0 {\n\t\tw.statusCode = statusCode\n\t}\n}\n\nfunc (w *bufferedResponseWriter) WriteToResponse(rw http.ResponseWriter) (int, error) {\n\th := rw.Header()\n\tfor k, vs := range w.header {\n\t\tfor _, v := range vs {\n\t\t\th.Add(k, v)\n\t\t}\n\t}\n\trw.WriteHeader(w.statusCode)\n\treturn rw.Write(w.data)\n}\n\nfunc newBufferedResponseWriter() *bufferedResponseWriter {\n\treturn &bufferedResponseWriter{\n\t\tstatusCode: 0,\n\t}\n}\n"
  },
  {
    "path": "server/webdav/file.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n)\n\n// slashClean is equivalent to but slightly more efficient than\n// path.Clean(\"/\" + name).\nfunc slashClean(name string) string {\n\tif name == \"\" || name[0] != '/' {\n\t\tname = \"/\" + name\n\t}\n\treturn path.Clean(name)\n}\n\n// moveFiles moves files and/or directories from src to dst.\n//\n// See section 9.9.4 for when various HTTP status codes apply.\nfunc moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int, err error) {\n\tsrcDir := path.Dir(src)\n\tdstDir := path.Dir(dst)\n\tsrcName := path.Base(src)\n\tdstName := path.Base(dst)\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tif srcDir != dstDir && !user.CanMove() {\n\t\treturn http.StatusForbidden, nil\n\t}\n\tif srcName != dstName && !user.CanRename() {\n\t\treturn http.StatusForbidden, nil\n\t}\n\tif srcDir == dstDir {\n\t\terr = fs.Rename(ctx, src, dstName)\n\t} else {\n\t\t_, err = fs.Move(context.WithValue(ctx, conf.NoTaskKey, struct{}{}), src, dstDir)\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\tif srcName != dstName {\n\t\t\terr = fs.Rename(ctx, path.Join(dstDir, srcName), dstName)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\t// TODO if there are no files copy, should return 204\n\treturn http.StatusCreated, nil\n}\n\n// copyFiles copies files and/or directories from src to dst.\n//\n// See section 9.8.5 for when various HTTP status codes apply.\nfunc copyFiles(ctx context.Context, src, dst string, overwrite bool) (status int, err error) {\n\tdstDir := path.Dir(dst)\n\t_, err = fs.Copy(context.WithValue(ctx, conf.NoTaskKey, struct{}{}), src, dstDir)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\t// TODO if there are no files copy, should return 204\n\treturn http.StatusCreated, nil\n}\n\n// walkFS traverses filesystem fs starting at name up to depth levels.\n//\n// Allowed values for depth are 0, 1 or infiniteDepth. For each visited node,\n// walkFS calls walkFn. If a visited file system node is a directory and\n// walkFn returns path.SkipDir, walkFS will skip traversal of this node.\nfunc walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn func(reqPath string, info model.Obj, err error) error) error {\n\t// This implementation is based on Walk's code in the standard path/path package.\n\terr := walkFn(name, info, nil)\n\tif err != nil {\n\t\tif info.IsDir() && err == filepath.SkipDir {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tif !info.IsDir() || depth == 0 {\n\t\treturn nil\n\t}\n\tif depth == 1 {\n\t\tdepth = 0\n\t}\n\tmeta, _ := op.GetNearestMeta(name)\n\t// Read directory names.\n\tobjs, err := fs.List(context.WithValue(ctx, conf.MetaKey, meta), name, &fs.ListArgs{})\n\t//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)\n\t//if err != nil {\n\t//\treturn walkFn(name, info, err)\n\t//}\n\t//fileInfos, err := f.Readdir(0)\n\t//f.Close()\n\tif err != nil {\n\t\treturn walkFn(name, info, err)\n\t}\n\n\tfor _, fileInfo := range objs {\n\t\tfilename := path.Join(name, fileInfo.GetName())\n\t\tif err != nil {\n\t\t\tif err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = walkFS(ctx, depth, filename, fileInfo, walkFn)\n\t\t\tif err != nil {\n\t\t\t\tif !fileInfo.IsDir() || err != filepath.SkipDir {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/webdav/if.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\n// The If header is covered by Section 10.4.\n// http://www.webdav.org/specs/rfc4918.html#HEADER_If\n\nimport (\n\t\"strings\"\n)\n\n// ifHeader is a disjunction (OR) of ifLists.\ntype ifHeader struct {\n\tlists []ifList\n}\n\n// ifList is a conjunction (AND) of Conditions, and an optional resource tag.\ntype ifList struct {\n\tresourceTag string\n\tconditions  []Condition\n}\n\n// parseIfHeader parses the \"If: foo bar\" HTTP header. The httpHeader string\n// should omit the \"If:\" prefix and have any \"\\r\\n\"s collapsed to a \" \", as is\n// returned by req.Header.Get(\"If\") for a http.Request req.\nfunc parseIfHeader(httpHeader string) (h ifHeader, ok bool) {\n\ts := strings.TrimSpace(httpHeader)\n\tswitch tokenType, _, _ := lex(s); tokenType {\n\tcase '(':\n\t\treturn parseNoTagLists(s)\n\tcase angleTokenType:\n\t\treturn parseTaggedLists(s)\n\tdefault:\n\t\treturn ifHeader{}, false\n\t}\n}\n\nfunc parseNoTagLists(s string) (h ifHeader, ok bool) {\n\tfor {\n\t\tl, remaining, ok := parseList(s)\n\t\tif !ok {\n\t\t\treturn ifHeader{}, false\n\t\t}\n\t\th.lists = append(h.lists, l)\n\t\tif remaining == \"\" {\n\t\t\treturn h, true\n\t\t}\n\t\ts = remaining\n\t}\n}\n\nfunc parseTaggedLists(s string) (h ifHeader, ok bool) {\n\tresourceTag, n := \"\", 0\n\tfor first := true; ; first = false {\n\t\ttokenType, tokenStr, remaining := lex(s)\n\t\tswitch tokenType {\n\t\tcase angleTokenType:\n\t\t\tif !first && n == 0 {\n\t\t\t\treturn ifHeader{}, false\n\t\t\t}\n\t\t\tresourceTag, n = tokenStr, 0\n\t\t\ts = remaining\n\t\tcase '(':\n\t\t\tn++\n\t\t\tl, remaining, ok := parseList(s)\n\t\t\tif !ok {\n\t\t\t\treturn ifHeader{}, false\n\t\t\t}\n\t\t\tl.resourceTag = resourceTag\n\t\t\th.lists = append(h.lists, l)\n\t\t\tif remaining == \"\" {\n\t\t\t\treturn h, true\n\t\t\t}\n\t\t\ts = remaining\n\t\tdefault:\n\t\t\treturn ifHeader{}, false\n\t\t}\n\t}\n}\n\nfunc parseList(s string) (l ifList, remaining string, ok bool) {\n\ttokenType, _, s := lex(s)\n\tif tokenType != '(' {\n\t\treturn ifList{}, \"\", false\n\t}\n\tfor {\n\t\ttokenType, _, remaining = lex(s)\n\t\tif tokenType == ')' {\n\t\t\tif len(l.conditions) == 0 {\n\t\t\t\treturn ifList{}, \"\", false\n\t\t\t}\n\t\t\treturn l, remaining, true\n\t\t}\n\t\tc, remaining, ok := parseCondition(s)\n\t\tif !ok {\n\t\t\treturn ifList{}, \"\", false\n\t\t}\n\t\tl.conditions = append(l.conditions, c)\n\t\ts = remaining\n\t}\n}\n\nfunc parseCondition(s string) (c Condition, remaining string, ok bool) {\n\ttokenType, tokenStr, s := lex(s)\n\tif tokenType == notTokenType {\n\t\tc.Not = true\n\t\ttokenType, tokenStr, s = lex(s)\n\t}\n\tswitch tokenType {\n\tcase strTokenType, angleTokenType:\n\t\tc.Token = tokenStr\n\tcase squareTokenType:\n\t\tc.ETag = tokenStr\n\tdefault:\n\t\treturn Condition{}, \"\", false\n\t}\n\treturn c, s, true\n}\n\n// Single-rune tokens like '(' or ')' have a token type equal to their rune.\n// All other tokens have a negative token type.\nconst (\n\terrTokenType    = rune(-1)\n\teofTokenType    = rune(-2)\n\tstrTokenType    = rune(-3)\n\tnotTokenType    = rune(-4)\n\tangleTokenType  = rune(-5)\n\tsquareTokenType = rune(-6)\n)\n\nfunc lex(s string) (tokenType rune, tokenStr string, remaining string) {\n\t// The net/textproto Reader that parses the HTTP header will collapse\n\t// Linear White Space that spans multiple \"\\r\\n\" lines to a single \" \",\n\t// so we don't need to look for '\\r' or '\\n'.\n\tfor len(s) > 0 && (s[0] == '\\t' || s[0] == ' ') {\n\t\ts = s[1:]\n\t}\n\tif len(s) == 0 {\n\t\treturn eofTokenType, \"\", \"\"\n\t}\n\ti := 0\nloop:\n\tfor ; i < len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '\\t', ' ', '(', ')', '<', '>', '[', ']':\n\t\t\tbreak loop\n\t\t}\n\t}\n\n\tif i != 0 {\n\t\ttokenStr, remaining = s[:i], s[i:]\n\t\tif tokenStr == \"Not\" {\n\t\t\treturn notTokenType, \"\", remaining\n\t\t}\n\t\treturn strTokenType, tokenStr, remaining\n\t}\n\n\tj := 0\n\tswitch s[0] {\n\tcase '<':\n\t\tj, tokenType = strings.IndexByte(s, '>'), angleTokenType\n\tcase '[':\n\t\tj, tokenType = strings.IndexByte(s, ']'), squareTokenType\n\tdefault:\n\t\treturn rune(s[0]), \"\", s[1:]\n\t}\n\tif j < 0 {\n\t\treturn errTokenType, \"\", \"\"\n\t}\n\treturn tokenType, s[1:j], s[j+1:]\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/README",
    "content": "This is a fork of the encoding/xml package at ca1d6c4, the last commit before\nhttps://go.googlesource.com/go/+/c0d6d33 \"encoding/xml: restore Go 1.4 name\nspace behavior\" made late in the lead-up to the Go 1.5 release.\n\nThe list of encoding/xml changes is at\nhttps://go.googlesource.com/go/+log/master/src/encoding/xml\n\nThis fork is temporary, and I (nigeltao) expect to revert it after Go 1.6 is\nreleased.\n\nSee http://golang.org/issue/11841\n"
  },
  {
    "path": "server/webdav/internal/xml/atom_test.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport \"time\"\n\nvar atomValue = &Feed{\n\tXMLName: Name{\"http://www.w3.org/2005/Atom\", \"feed\"},\n\tTitle:   \"Example Feed\",\n\tLink:    []Link{{Href: \"http://example.org/\"}},\n\tUpdated: ParseTime(\"2003-12-13T18:30:02Z\"),\n\tAuthor:  Person{Name: \"John Doe\"},\n\tId:      \"urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6\",\n\n\tEntry: []Entry{\n\t\t{\n\t\t\tTitle:   \"Atom-Powered Robots Run Amok\",\n\t\t\tLink:    []Link{{Href: \"http://example.org/2003/12/13/atom03\"}},\n\t\t\tId:      \"urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a\",\n\t\t\tUpdated: ParseTime(\"2003-12-13T18:30:02Z\"),\n\t\t\tSummary: NewText(\"Some text.\"),\n\t\t},\n\t},\n}\n\nvar atomXml = `` +\n\t`<feed xmlns=\"http://www.w3.org/2005/Atom\" updated=\"2003-12-13T18:30:02Z\">` +\n\t`<title>Example Feed</title>` +\n\t`<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>` +\n\t`<link href=\"http://example.org/\"></link>` +\n\t`<author><name>John Doe</name><uri></uri><email></email></author>` +\n\t`<entry>` +\n\t`<title>Atom-Powered Robots Run Amok</title>` +\n\t`<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>` +\n\t`<link href=\"http://example.org/2003/12/13/atom03\"></link>` +\n\t`<updated>2003-12-13T18:30:02Z</updated>` +\n\t`<author><name></name><uri></uri><email></email></author>` +\n\t`<summary>Some text.</summary>` +\n\t`</entry>` +\n\t`</feed>`\n\nfunc ParseTime(str string) time.Time {\n\tt, err := time.Parse(time.RFC3339, str)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn t\n}\n\nfunc NewText(text string) Text {\n\treturn Text{\n\t\tBody: text,\n\t}\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/example_test.go",
    "content": "// Copyright 2012 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml_test\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc ExampleMarshalIndent() {\n\ttype Address struct {\n\t\tCity, State string\n\t}\n\ttype Person struct {\n\t\tXMLName   xml.Name `xml:\"person\"`\n\t\tId        int      `xml:\"id,attr\"`\n\t\tFirstName string   `xml:\"name>first\"`\n\t\tLastName  string   `xml:\"name>last\"`\n\t\tAge       int      `xml:\"age\"`\n\t\tHeight    float32  `xml:\"height,omitempty\"`\n\t\tMarried   bool\n\t\tAddress\n\t\tComment string `xml:\",comment\"`\n\t}\n\n\tv := &Person{Id: 13, FirstName: \"John\", LastName: \"Doe\", Age: 42}\n\tv.Comment = \" Need more details. \"\n\tv.Address = Address{\"Hanga Roa\", \"Easter Island\"}\n\n\toutput, err := xml.MarshalIndent(v, \"  \", \"    \")\n\tif err != nil {\n\t\tfmt.Printf(\"error: %v\\n\", err)\n\t}\n\n\tos.Stdout.Write(output)\n\t// Output:\n\t//   <person id=\"13\">\n\t//       <name>\n\t//           <first>John</first>\n\t//           <last>Doe</last>\n\t//       </name>\n\t//       <age>42</age>\n\t//       <Married>false</Married>\n\t//       <City>Hanga Roa</City>\n\t//       <State>Easter Island</State>\n\t//       <!-- Need more details. -->\n\t//   </person>\n}\n\nfunc ExampleEncoder() {\n\ttype Address struct {\n\t\tCity, State string\n\t}\n\ttype Person struct {\n\t\tXMLName   xml.Name `xml:\"person\"`\n\t\tId        int      `xml:\"id,attr\"`\n\t\tFirstName string   `xml:\"name>first\"`\n\t\tLastName  string   `xml:\"name>last\"`\n\t\tAge       int      `xml:\"age\"`\n\t\tHeight    float32  `xml:\"height,omitempty\"`\n\t\tMarried   bool\n\t\tAddress\n\t\tComment string `xml:\",comment\"`\n\t}\n\n\tv := &Person{Id: 13, FirstName: \"John\", LastName: \"Doe\", Age: 42}\n\tv.Comment = \" Need more details. \"\n\tv.Address = Address{\"Hanga Roa\", \"Easter Island\"}\n\n\tenc := xml.NewEncoder(os.Stdout)\n\tenc.Indent(\"  \", \"    \")\n\tif err := enc.Encode(v); err != nil {\n\t\tfmt.Printf(\"error: %v\\n\", err)\n\t}\n\n\t// Output:\n\t//   <person id=\"13\">\n\t//       <name>\n\t//           <first>John</first>\n\t//           <last>Doe</last>\n\t//       </name>\n\t//       <age>42</age>\n\t//       <Married>false</Married>\n\t//       <City>Hanga Roa</City>\n\t//       <State>Easter Island</State>\n\t//       <!-- Need more details. -->\n\t//   </person>\n}\n\n// This example demonstrates unmarshaling an XML excerpt into a value with\n// some preset fields. Note that the Phone field isn't modified and that\n// the XML <Company> element is ignored. Also, the Groups field is assigned\n// considering the element path provided in its tag.\nfunc ExampleUnmarshal() {\n\ttype Email struct {\n\t\tWhere string `xml:\"where,attr\"`\n\t\tAddr  string\n\t}\n\ttype Address struct {\n\t\tCity, State string\n\t}\n\ttype Result struct {\n\t\tXMLName xml.Name `xml:\"Person\"`\n\t\tName    string   `xml:\"FullName\"`\n\t\tPhone   string\n\t\tEmail   []Email\n\t\tGroups  []string `xml:\"Group>Value\"`\n\t\tAddress\n\t}\n\tv := Result{Name: \"none\", Phone: \"none\"}\n\n\tdata := `\n\t\t<Person>\n\t\t\t<FullName>Grace R. Emlin</FullName>\n\t\t\t<Company>Example Inc.</Company>\n\t\t\t<Email where=\"home\">\n\t\t\t\t<Addr>gre@example.com</Addr>\n\t\t\t</Email>\n\t\t\t<Email where='work'>\n\t\t\t\t<Addr>gre@work.com</Addr>\n\t\t\t</Email>\n\t\t\t<Group>\n\t\t\t\t<Value>Friends</Value>\n\t\t\t\t<Value>Squash</Value>\n\t\t\t</Group>\n\t\t\t<City>Hanga Roa</City>\n\t\t\t<State>Easter Island</State>\n\t\t</Person>\n\t`\n\terr := xml.Unmarshal([]byte(data), &v)\n\tif err != nil {\n\t\tfmt.Printf(\"error: %v\", err)\n\t\treturn\n\t}\n\tfmt.Printf(\"XMLName: %#v\\n\", v.XMLName)\n\tfmt.Printf(\"Name: %q\\n\", v.Name)\n\tfmt.Printf(\"Phone: %q\\n\", v.Phone)\n\tfmt.Printf(\"Email: %v\\n\", v.Email)\n\tfmt.Printf(\"Groups: %v\\n\", v.Groups)\n\tfmt.Printf(\"Address: %v\\n\", v.Address)\n\t// Output:\n\t// XMLName: xml.Name{Space:\"\", Local:\"Person\"}\n\t// Name: \"Grace R. Emlin\"\n\t// Phone: \"none\"\n\t// Email: [{home gre@example.com} {work gre@work.com}]\n\t// Groups: [Friends Squash]\n\t// Address: {Hanga Roa Easter Island}\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/marshal.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\t// A generic XML header suitable for use with the output of Marshal.\n\t// This is not automatically added to any output of this package,\n\t// it is provided as a convenience.\n\tHeader = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>` + \"\\n\"\n)\n\n// Marshal returns the XML encoding of v.\n//\n// Marshal handles an array or slice by marshalling each of the elements.\n// Marshal handles a pointer by marshalling the value it points at or, if the\n// pointer is nil, by writing nothing. Marshal handles an interface value by\n// marshalling the value it contains or, if the interface value is nil, by\n// writing nothing. Marshal handles all other data by writing one or more XML\n// elements containing the data.\n//\n// The name for the XML elements is taken from, in order of preference:\n//   - the tag on the XMLName field, if the data is a struct\n//   - the value of the XMLName field of type xml.Name\n//   - the tag of the struct field used to obtain the data\n//   - the name of the struct field used to obtain the data\n//   - the name of the marshalled type\n//\n// The XML element for a struct contains marshalled elements for each of the\n// exported fields of the struct, with these exceptions:\n//   - the XMLName field, described above, is omitted.\n//   - a field with tag \"-\" is omitted.\n//   - a field with tag \"name,attr\" becomes an attribute with\n//     the given name in the XML element.\n//   - a field with tag \",attr\" becomes an attribute with the\n//     field name in the XML element.\n//   - a field with tag \",chardata\" is written as character data,\n//     not as an XML element.\n//   - a field with tag \",innerxml\" is written verbatim, not subject\n//     to the usual marshalling procedure.\n//   - a field with tag \",comment\" is written as an XML comment, not\n//     subject to the usual marshalling procedure. It must not contain\n//     the \"--\" string within it.\n//   - a field with a tag including the \"omitempty\" option is omitted\n//     if the field value is empty. The empty values are false, 0, any\n//     nil pointer or interface value, and any array, slice, map, or\n//     string of length zero.\n//   - an anonymous struct field is handled as if the fields of its\n//     value were part of the outer struct.\n//\n// If a field uses a tag \"a>b>c\", then the element c will be nested inside\n// parent elements a and b. Fields that appear next to each other that name\n// the same parent will be enclosed in one XML element.\n//\n// See MarshalIndent for an example.\n//\n// Marshal will return an error if asked to marshal a channel, function, or map.\nfunc Marshal(v interface{}) ([]byte, error) {\n\tvar b bytes.Buffer\n\tif err := NewEncoder(&b).Encode(v); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b.Bytes(), nil\n}\n\n// Marshaler is the interface implemented by objects that can marshal\n// themselves into valid XML elements.\n//\n// MarshalXML encodes the receiver as zero or more XML elements.\n// By convention, arrays or slices are typically encoded as a sequence\n// of elements, one per entry.\n// Using start as the element tag is not required, but doing so\n// will enable Unmarshal to match the XML elements to the correct\n// struct field.\n// One common implementation strategy is to construct a separate\n// value with a layout corresponding to the desired XML and then\n// to encode it using e.EncodeElement.\n// Another common strategy is to use repeated calls to e.EncodeToken\n// to generate the XML output one token at a time.\n// The sequence of encoded tokens must make up zero or more valid\n// XML elements.\ntype Marshaler interface {\n\tMarshalXML(e *Encoder, start StartElement) error\n}\n\n// MarshalerAttr is the interface implemented by objects that can marshal\n// themselves into valid XML attributes.\n//\n// MarshalXMLAttr returns an XML attribute with the encoded value of the receiver.\n// Using name as the attribute name is not required, but doing so\n// will enable Unmarshal to match the attribute to the correct\n// struct field.\n// If MarshalXMLAttr returns the zero attribute Attr{}, no attribute\n// will be generated in the output.\n// MarshalXMLAttr is used only for struct fields with the\n// \"attr\" option in the field tag.\ntype MarshalerAttr interface {\n\tMarshalXMLAttr(name Name) (Attr, error)\n}\n\n// MarshalIndent works like Marshal, but each XML element begins on a new\n// indented line that starts with prefix and is followed by one or more\n// copies of indent according to the nesting depth.\nfunc MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {\n\tvar b bytes.Buffer\n\tenc := NewEncoder(&b)\n\tenc.Indent(prefix, indent)\n\tif err := enc.Encode(v); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b.Bytes(), nil\n}\n\n// An Encoder writes XML data to an output stream.\ntype Encoder struct {\n\tp printer\n}\n\n// NewEncoder returns a new encoder that writes to w.\nfunc NewEncoder(w io.Writer) *Encoder {\n\te := &Encoder{printer{Writer: bufio.NewWriter(w)}}\n\te.p.encoder = e\n\treturn e\n}\n\n// Indent sets the encoder to generate XML in which each element\n// begins on a new indented line that starts with prefix and is followed by\n// one or more copies of indent according to the nesting depth.\nfunc (enc *Encoder) Indent(prefix, indent string) {\n\tenc.p.prefix = prefix\n\tenc.p.indent = indent\n}\n\n// Encode writes the XML encoding of v to the stream.\n//\n// See the documentation for Marshal for details about the conversion\n// of Go values to XML.\n//\n// Encode calls Flush before returning.\nfunc (enc *Encoder) Encode(v interface{}) error {\n\terr := enc.p.marshalValue(reflect.ValueOf(v), nil, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn enc.p.Flush()\n}\n\n// EncodeElement writes the XML encoding of v to the stream,\n// using start as the outermost tag in the encoding.\n//\n// See the documentation for Marshal for details about the conversion\n// of Go values to XML.\n//\n// EncodeElement calls Flush before returning.\nfunc (enc *Encoder) EncodeElement(v interface{}, start StartElement) error {\n\terr := enc.p.marshalValue(reflect.ValueOf(v), nil, &start)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn enc.p.Flush()\n}\n\nvar (\n\tbegComment   = []byte(\"<!--\")\n\tendComment   = []byte(\"-->\")\n\tendProcInst  = []byte(\"?>\")\n\tendDirective = []byte(\">\")\n)\n\n// EncodeToken writes the given XML token to the stream.\n// It returns an error if StartElement and EndElement tokens are not\n// properly matched.\n//\n// EncodeToken does not call Flush, because usually it is part of a\n// larger operation such as Encode or EncodeElement (or a custom\n// Marshaler's MarshalXML invoked during those), and those will call\n// Flush when finished. Callers that create an Encoder and then invoke\n// EncodeToken directly, without using Encode or EncodeElement, need to\n// call Flush when finished to ensure that the XML is written to the\n// underlying writer.\n//\n// EncodeToken allows writing a ProcInst with Target set to \"xml\" only\n// as the first token in the stream.\n//\n// When encoding a StartElement holding an XML namespace prefix\n// declaration for a prefix that is not already declared, contained\n// elements (including the StartElement itself) will use the declared\n// prefix when encoding names with matching namespace URIs.\nfunc (enc *Encoder) EncodeToken(t Token) error {\n\n\tp := &enc.p\n\tswitch t := t.(type) {\n\tcase StartElement:\n\t\tif err := p.writeStart(&t); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase EndElement:\n\t\tif err := p.writeEnd(t.Name); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase CharData:\n\t\tescapeText(p, t, false)\n\tcase Comment:\n\t\tif bytes.Contains(t, endComment) {\n\t\t\treturn fmt.Errorf(\"xml: EncodeToken of Comment containing --> marker\")\n\t\t}\n\t\tp.WriteString(\"<!--\")\n\t\tp.Write(t)\n\t\tp.WriteString(\"-->\")\n\t\treturn p.cachedWriteError()\n\tcase ProcInst:\n\t\t// First token to be encoded which is also a ProcInst with target of xml\n\t\t// is the xml declaration. The only ProcInst where target of xml is allowed.\n\t\tif t.Target == \"xml\" && p.Buffered() != 0 {\n\t\t\treturn fmt.Errorf(\"xml: EncodeToken of ProcInst xml target only valid for xml declaration, first token encoded\")\n\t\t}\n\t\tif !isNameString(t.Target) {\n\t\t\treturn fmt.Errorf(\"xml: EncodeToken of ProcInst with invalid Target\")\n\t\t}\n\t\tif bytes.Contains(t.Inst, endProcInst) {\n\t\t\treturn fmt.Errorf(\"xml: EncodeToken of ProcInst containing ?> marker\")\n\t\t}\n\t\tp.WriteString(\"<?\")\n\t\tp.WriteString(t.Target)\n\t\tif len(t.Inst) > 0 {\n\t\t\tp.WriteByte(' ')\n\t\t\tp.Write(t.Inst)\n\t\t}\n\t\tp.WriteString(\"?>\")\n\tcase Directive:\n\t\tif !isValidDirective(t) {\n\t\t\treturn fmt.Errorf(\"xml: EncodeToken of Directive containing wrong < or > markers\")\n\t\t}\n\t\tp.WriteString(\"<!\")\n\t\tp.Write(t)\n\t\tp.WriteString(\">\")\n\tdefault:\n\t\treturn fmt.Errorf(\"xml: EncodeToken of invalid token type\")\n\n\t}\n\treturn p.cachedWriteError()\n}\n\n// isValidDirective reports whether dir is a valid directive text,\n// meaning angle brackets are matched, ignoring comments and strings.\nfunc isValidDirective(dir Directive) bool {\n\tvar (\n\t\tdepth     int\n\t\tinquote   uint8\n\t\tincomment bool\n\t)\n\tfor i, c := range dir {\n\t\tswitch {\n\t\tcase incomment:\n\t\t\tif c == '>' {\n\t\t\t\tif n := 1 + i - len(endComment); n >= 0 && bytes.Equal(dir[n:i+1], endComment) {\n\t\t\t\t\tincomment = false\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Just ignore anything in comment\n\t\tcase inquote != 0:\n\t\t\tif c == inquote {\n\t\t\t\tinquote = 0\n\t\t\t}\n\t\t\t// Just ignore anything within quotes\n\t\tcase c == '\\'' || c == '\"':\n\t\t\tinquote = c\n\t\tcase c == '<':\n\t\t\tif i+len(begComment) < len(dir) && bytes.Equal(dir[i:i+len(begComment)], begComment) {\n\t\t\t\tincomment = true\n\t\t\t} else {\n\t\t\t\tdepth++\n\t\t\t}\n\t\tcase c == '>':\n\t\t\tif depth == 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tdepth--\n\t\t}\n\t}\n\treturn depth == 0 && inquote == 0 && !incomment\n}\n\n// Flush flushes any buffered XML to the underlying writer.\n// See the EncodeToken documentation for details about when it is necessary.\nfunc (enc *Encoder) Flush() error {\n\treturn enc.p.Flush()\n}\n\ntype printer struct {\n\t*bufio.Writer\n\tencoder    *Encoder\n\tseq        int\n\tindent     string\n\tprefix     string\n\tdepth      int\n\tindentedIn bool\n\tputNewline bool\n\tdefaultNS  string\n\tattrNS     map[string]string // map prefix -> name space\n\tattrPrefix map[string]string // map name space -> prefix\n\tprefixes   []printerPrefix\n\ttags       []Name\n}\n\n// printerPrefix holds a namespace undo record.\n// When an element is popped, the prefix record\n// is set back to the recorded URL. The empty\n// prefix records the URL for the default name space.\n//\n// The start of an element is recorded with an element\n// that has mark=true.\ntype printerPrefix struct {\n\tprefix string\n\turl    string\n\tmark   bool\n}\n\nfunc (p *printer) prefixForNS(url string, isAttr bool) string {\n\t// The \"http://www.w3.org/XML/1998/namespace\" name space is predefined as \"xml\"\n\t// and must be referred to that way.\n\t// (The \"http://www.w3.org/2000/xmlns/\" name space is also predefined as \"xmlns\",\n\t// but users should not be trying to use that one directly - that's our job.)\n\tif url == xmlURL {\n\t\treturn \"xml\"\n\t}\n\tif !isAttr && url == p.defaultNS {\n\t\t// We can use the default name space.\n\t\treturn \"\"\n\t}\n\treturn p.attrPrefix[url]\n}\n\n// defineNS pushes any namespace definition found in the given attribute.\n// If ignoreNonEmptyDefault is true, an xmlns=\"nonempty\"\n// attribute will be ignored.\nfunc (p *printer) defineNS(attr Attr, ignoreNonEmptyDefault bool) error {\n\tvar prefix string\n\tif attr.Name.Local == \"xmlns\" {\n\t\tif attr.Name.Space != \"\" && attr.Name.Space != \"xml\" && attr.Name.Space != xmlURL {\n\t\t\treturn fmt.Errorf(\"xml: cannot redefine xmlns attribute prefix\")\n\t\t}\n\t} else if attr.Name.Space == \"xmlns\" && attr.Name.Local != \"\" {\n\t\tprefix = attr.Name.Local\n\t\tif attr.Value == \"\" {\n\t\t\t// Technically, an empty XML namespace is allowed for an attribute.\n\t\t\t// From http://www.w3.org/TR/xml-names11/#scoping-defaulting:\n\t\t\t//\n\t\t\t// \tThe attribute value in a namespace declaration for a prefix may be\n\t\t\t//\tempty. This has the effect, within the scope of the declaration, of removing\n\t\t\t//\tany association of the prefix with a namespace name.\n\t\t\t//\n\t\t\t// However our namespace prefixes here are used only as hints. There's\n\t\t\t// no need to respect the removal of a namespace prefix, so we ignore it.\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\t// Ignore: it's not a namespace definition\n\t\treturn nil\n\t}\n\tif prefix == \"\" {\n\t\tif attr.Value == p.defaultNS {\n\t\t\t// No need for redefinition.\n\t\t\treturn nil\n\t\t}\n\t\tif attr.Value != \"\" && ignoreNonEmptyDefault {\n\t\t\t// We have an xmlns=\"...\" value but\n\t\t\t// it can't define a name space in this context,\n\t\t\t// probably because the element has an empty\n\t\t\t// name space. In this case, we just ignore\n\t\t\t// the name space declaration.\n\t\t\treturn nil\n\t\t}\n\t} else if _, ok := p.attrPrefix[attr.Value]; ok {\n\t\t// There's already a prefix for the given name space,\n\t\t// so use that. This prevents us from\n\t\t// having two prefixes for the same name space\n\t\t// so attrNS and attrPrefix can remain bijective.\n\t\treturn nil\n\t}\n\tp.pushPrefix(prefix, attr.Value)\n\treturn nil\n}\n\n// createNSPrefix creates a name space prefix attribute\n// to use for the given name space, defining a new prefix\n// if necessary.\n// If isAttr is true, the prefix is to be created for an attribute\n// prefix, which means that the default name space cannot\n// be used.\nfunc (p *printer) createNSPrefix(url string, isAttr bool) {\n\tif _, ok := p.attrPrefix[url]; ok {\n\t\t// We already have a prefix for the given URL.\n\t\treturn\n\t}\n\tswitch {\n\tcase !isAttr && url == p.defaultNS:\n\t\t// We can use the default name space.\n\t\treturn\n\tcase url == \"\":\n\t\t// The only way we can encode names in the empty\n\t\t// name space is by using the default name space,\n\t\t// so we must use that.\n\t\tif p.defaultNS != \"\" {\n\t\t\t// The default namespace is non-empty, so we\n\t\t\t// need to set it to empty.\n\t\t\tp.pushPrefix(\"\", \"\")\n\t\t}\n\t\treturn\n\tcase url == xmlURL:\n\t\treturn\n\t}\n\t// TODO If the URL is an existing prefix, we could\n\t// use it as is. That would enable the\n\t// marshaling of elements that had been unmarshaled\n\t// and with a name space prefix that was not found.\n\t// although technically it would be incorrect.\n\n\t// Pick a name. We try to use the final element of the path\n\t// but fall back to _.\n\tprefix := strings.TrimRight(url, \"/\")\n\tif i := strings.LastIndex(prefix, \"/\"); i >= 0 {\n\t\tprefix = prefix[i+1:]\n\t}\n\tif prefix == \"\" || !isName([]byte(prefix)) || strings.Contains(prefix, \":\") {\n\t\tprefix = \"_\"\n\t}\n\tif strings.HasPrefix(prefix, \"xml\") {\n\t\t// xmlanything is reserved.\n\t\tprefix = \"_\" + prefix\n\t}\n\tif p.attrNS[prefix] != \"\" {\n\t\t// Name is taken. Find a better one.\n\t\tfor p.seq++; ; p.seq++ {\n\t\t\tif id := prefix + \"_\" + strconv.Itoa(p.seq); p.attrNS[id] == \"\" {\n\t\t\t\tprefix = id\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tp.pushPrefix(prefix, url)\n}\n\n// writeNamespaces writes xmlns attributes for all the\n// namespace prefixes that have been defined in\n// the current element.\nfunc (p *printer) writeNamespaces() {\n\tfor i := len(p.prefixes) - 1; i >= 0; i-- {\n\t\tprefix := p.prefixes[i]\n\t\tif prefix.mark {\n\t\t\treturn\n\t\t}\n\t\tp.WriteString(\" \")\n\t\tif prefix.prefix == \"\" {\n\t\t\t// Default name space.\n\t\t\tp.WriteString(`xmlns=\"`)\n\t\t} else {\n\t\t\tp.WriteString(\"xmlns:\")\n\t\t\tp.WriteString(prefix.prefix)\n\t\t\tp.WriteString(`=\"`)\n\t\t}\n\t\tEscapeText(p, []byte(p.nsForPrefix(prefix.prefix)))\n\t\tp.WriteString(`\"`)\n\t}\n}\n\n// pushPrefix pushes a new prefix on the prefix stack\n// without checking to see if it is already defined.\nfunc (p *printer) pushPrefix(prefix, url string) {\n\tp.prefixes = append(p.prefixes, printerPrefix{\n\t\tprefix: prefix,\n\t\turl:    p.nsForPrefix(prefix),\n\t})\n\tp.setAttrPrefix(prefix, url)\n}\n\n// nsForPrefix returns the name space for the given\n// prefix. Note that this is not valid for the\n// empty attribute prefix, which always has an empty\n// name space.\nfunc (p *printer) nsForPrefix(prefix string) string {\n\tif prefix == \"\" {\n\t\treturn p.defaultNS\n\t}\n\treturn p.attrNS[prefix]\n}\n\n// markPrefix marks the start of an element on the prefix\n// stack.\nfunc (p *printer) markPrefix() {\n\tp.prefixes = append(p.prefixes, printerPrefix{\n\t\tmark: true,\n\t})\n}\n\n// popPrefix pops all defined prefixes for the current\n// element.\nfunc (p *printer) popPrefix() {\n\tfor len(p.prefixes) > 0 {\n\t\tprefix := p.prefixes[len(p.prefixes)-1]\n\t\tp.prefixes = p.prefixes[:len(p.prefixes)-1]\n\t\tif prefix.mark {\n\t\t\tbreak\n\t\t}\n\t\tp.setAttrPrefix(prefix.prefix, prefix.url)\n\t}\n}\n\n// setAttrPrefix sets an attribute name space prefix.\n// If url is empty, the attribute is removed.\n// If prefix is empty, the default name space is set.\nfunc (p *printer) setAttrPrefix(prefix, url string) {\n\tif prefix == \"\" {\n\t\tp.defaultNS = url\n\t\treturn\n\t}\n\tif url == \"\" {\n\t\tdelete(p.attrPrefix, p.attrNS[prefix])\n\t\tdelete(p.attrNS, prefix)\n\t\treturn\n\t}\n\tif p.attrPrefix == nil {\n\t\t// Need to define a new name space.\n\t\tp.attrPrefix = make(map[string]string)\n\t\tp.attrNS = make(map[string]string)\n\t}\n\t// Remove any old prefix value. This is OK because we maintain a\n\t// strict one-to-one mapping between prefix and URL (see\n\t// defineNS)\n\tdelete(p.attrPrefix, p.attrNS[prefix])\n\tp.attrPrefix[url] = prefix\n\tp.attrNS[prefix] = url\n}\n\nvar (\n\tmarshalerType     = reflect.TypeOf((*Marshaler)(nil)).Elem()\n\tmarshalerAttrType = reflect.TypeOf((*MarshalerAttr)(nil)).Elem()\n\ttextMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()\n)\n\n// marshalValue writes one or more XML elements representing val.\n// If val was obtained from a struct field, finfo must have its details.\nfunc (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplate *StartElement) error {\n\tif startTemplate != nil && startTemplate.Name.Local == \"\" {\n\t\treturn fmt.Errorf(\"xml: EncodeElement of StartElement with missing name\")\n\t}\n\n\tif !val.IsValid() {\n\t\treturn nil\n\t}\n\tif finfo != nil && finfo.flags&fOmitEmpty != 0 && isEmptyValue(val) {\n\t\treturn nil\n\t}\n\n\t// Drill into interfaces and pointers.\n\t// This can turn into an infinite loop given a cyclic chain,\n\t// but it matches the Go 1 behavior.\n\tfor val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr {\n\t\tif val.IsNil() {\n\t\t\treturn nil\n\t\t}\n\t\tval = val.Elem()\n\t}\n\n\tkind := val.Kind()\n\ttyp := val.Type()\n\n\t// Check for marshaler.\n\tif val.CanInterface() && typ.Implements(marshalerType) {\n\t\treturn p.marshalInterface(val.Interface().(Marshaler), p.defaultStart(typ, finfo, startTemplate))\n\t}\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(marshalerType) {\n\t\t\treturn p.marshalInterface(pv.Interface().(Marshaler), p.defaultStart(pv.Type(), finfo, startTemplate))\n\t\t}\n\t}\n\n\t// Check for text marshaler.\n\tif val.CanInterface() && typ.Implements(textMarshalerType) {\n\t\treturn p.marshalTextInterface(val.Interface().(encoding.TextMarshaler), p.defaultStart(typ, finfo, startTemplate))\n\t}\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(textMarshalerType) {\n\t\t\treturn p.marshalTextInterface(pv.Interface().(encoding.TextMarshaler), p.defaultStart(pv.Type(), finfo, startTemplate))\n\t\t}\n\t}\n\n\t// Slices and arrays iterate over the elements. They do not have an enclosing tag.\n\tif (kind == reflect.Slice || kind == reflect.Array) && typ.Elem().Kind() != reflect.Uint8 {\n\t\tfor i, n := 0, val.Len(); i < n; i++ {\n\t\t\tif err := p.marshalValue(val.Index(i), finfo, startTemplate); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\ttinfo, err := getTypeInfo(typ)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create start element.\n\t// Precedence for the XML element name is:\n\t// 0. startTemplate\n\t// 1. XMLName field in underlying struct;\n\t// 2. field name/tag in the struct field; and\n\t// 3. type name\n\tvar start StartElement\n\n\t// explicitNS records whether the element's name space has been\n\t// explicitly set (for example an XMLName field).\n\texplicitNS := false\n\n\tif startTemplate != nil {\n\t\tstart.Name = startTemplate.Name\n\t\texplicitNS = true\n\t\tstart.Attr = append(start.Attr, startTemplate.Attr...)\n\t} else if tinfo.xmlname != nil {\n\t\txmlname := tinfo.xmlname\n\t\tif xmlname.name != \"\" {\n\t\t\tstart.Name.Space, start.Name.Local = xmlname.xmlns, xmlname.name\n\t\t} else if v, ok := xmlname.value(val).Interface().(Name); ok && v.Local != \"\" {\n\t\t\tstart.Name = v\n\t\t}\n\t\texplicitNS = true\n\t}\n\tif start.Name.Local == \"\" && finfo != nil {\n\t\tstart.Name.Local = finfo.name\n\t\tif finfo.xmlns != \"\" {\n\t\t\tstart.Name.Space = finfo.xmlns\n\t\t\texplicitNS = true\n\t\t}\n\t}\n\tif start.Name.Local == \"\" {\n\t\tname := typ.Name()\n\t\tif name == \"\" {\n\t\t\treturn &UnsupportedTypeError{typ}\n\t\t}\n\t\tstart.Name.Local = name\n\t}\n\n\t// defaultNS records the default name space as set by a xmlns=\"...\"\n\t// attribute. We don't set p.defaultNS because we want to let\n\t// the attribute writing code (in p.defineNS) be solely responsible\n\t// for maintaining that.\n\tdefaultNS := p.defaultNS\n\n\t// Attributes\n\tfor i := range tinfo.fields {\n\t\tfinfo := &tinfo.fields[i]\n\t\tif finfo.flags&fAttr == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tattr, err := p.fieldAttr(finfo, val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif attr.Name.Local == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tstart.Attr = append(start.Attr, attr)\n\t\tif attr.Name.Space == \"\" && attr.Name.Local == \"xmlns\" {\n\t\t\tdefaultNS = attr.Value\n\t\t}\n\t}\n\tif !explicitNS {\n\t\t// Historic behavior: elements use the default name space\n\t\t// they are contained in by default.\n\t\tstart.Name.Space = defaultNS\n\t}\n\t// Historic behaviour: an element that's in a namespace sets\n\t// the default namespace for all elements contained within it.\n\tstart.setDefaultNamespace()\n\n\tif err := p.writeStart(&start); err != nil {\n\t\treturn err\n\t}\n\n\tif val.Kind() == reflect.Struct {\n\t\terr = p.marshalStruct(tinfo, val)\n\t} else {\n\t\ts, b, err1 := p.marshalSimple(typ, val)\n\t\tif err1 != nil {\n\t\t\terr = err1\n\t\t} else if b != nil {\n\t\t\tEscapeText(p, b)\n\t\t} else {\n\t\t\tp.EscapeString(s)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.writeEnd(start.Name); err != nil {\n\t\treturn err\n\t}\n\n\treturn p.cachedWriteError()\n}\n\n// fieldAttr returns the attribute of the given field.\n// If the returned attribute has an empty Name.Local,\n// it should not be used.\n// The given value holds the value containing the field.\nfunc (p *printer) fieldAttr(finfo *fieldInfo, val reflect.Value) (Attr, error) {\n\tfv := finfo.value(val)\n\tname := Name{Space: finfo.xmlns, Local: finfo.name}\n\tif finfo.flags&fOmitEmpty != 0 && isEmptyValue(fv) {\n\t\treturn Attr{}, nil\n\t}\n\tif fv.Kind() == reflect.Interface && fv.IsNil() {\n\t\treturn Attr{}, nil\n\t}\n\tif fv.CanInterface() && fv.Type().Implements(marshalerAttrType) {\n\t\tattr, err := fv.Interface().(MarshalerAttr).MarshalXMLAttr(name)\n\t\treturn attr, err\n\t}\n\tif fv.CanAddr() {\n\t\tpv := fv.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(marshalerAttrType) {\n\t\t\tattr, err := pv.Interface().(MarshalerAttr).MarshalXMLAttr(name)\n\t\t\treturn attr, err\n\t\t}\n\t}\n\tif fv.CanInterface() && fv.Type().Implements(textMarshalerType) {\n\t\ttext, err := fv.Interface().(encoding.TextMarshaler).MarshalText()\n\t\tif err != nil {\n\t\t\treturn Attr{}, err\n\t\t}\n\t\treturn Attr{name, string(text)}, nil\n\t}\n\tif fv.CanAddr() {\n\t\tpv := fv.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(textMarshalerType) {\n\t\t\ttext, err := pv.Interface().(encoding.TextMarshaler).MarshalText()\n\t\t\tif err != nil {\n\t\t\t\treturn Attr{}, err\n\t\t\t}\n\t\t\treturn Attr{name, string(text)}, nil\n\t\t}\n\t}\n\t// Dereference or skip nil pointer, interface values.\n\tswitch fv.Kind() {\n\tcase reflect.Ptr, reflect.Interface:\n\t\tif fv.IsNil() {\n\t\t\treturn Attr{}, nil\n\t\t}\n\t\tfv = fv.Elem()\n\t}\n\ts, b, err := p.marshalSimple(fv.Type(), fv)\n\tif err != nil {\n\t\treturn Attr{}, err\n\t}\n\tif b != nil {\n\t\ts = string(b)\n\t}\n\treturn Attr{name, s}, nil\n}\n\n// defaultStart returns the default start element to use,\n// given the reflect type, field info, and start template.\nfunc (p *printer) defaultStart(typ reflect.Type, finfo *fieldInfo, startTemplate *StartElement) StartElement {\n\tvar start StartElement\n\t// Precedence for the XML element name is as above,\n\t// except that we do not look inside structs for the first field.\n\tif startTemplate != nil {\n\t\tstart.Name = startTemplate.Name\n\t\tstart.Attr = append(start.Attr, startTemplate.Attr...)\n\t} else if finfo != nil && finfo.name != \"\" {\n\t\tstart.Name.Local = finfo.name\n\t\tstart.Name.Space = finfo.xmlns\n\t} else if typ.Name() != \"\" {\n\t\tstart.Name.Local = typ.Name()\n\t} else {\n\t\t// Must be a pointer to a named type,\n\t\t// since it has the Marshaler methods.\n\t\tstart.Name.Local = typ.Elem().Name()\n\t}\n\t// Historic behaviour: elements use the name space of\n\t// the element they are contained in by default.\n\tif start.Name.Space == \"\" {\n\t\tstart.Name.Space = p.defaultNS\n\t}\n\tstart.setDefaultNamespace()\n\treturn start\n}\n\n// marshalInterface marshals a Marshaler interface value.\nfunc (p *printer) marshalInterface(val Marshaler, start StartElement) error {\n\t// Push a marker onto the tag stack so that MarshalXML\n\t// cannot close the XML tags that it did not open.\n\tp.tags = append(p.tags, Name{})\n\tn := len(p.tags)\n\n\terr := val.MarshalXML(p.encoder, start)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Make sure MarshalXML closed all its tags. p.tags[n-1] is the mark.\n\tif len(p.tags) > n {\n\t\treturn fmt.Errorf(\"xml: %s.MarshalXML wrote invalid XML: <%s> not closed\", receiverType(val), p.tags[len(p.tags)-1].Local)\n\t}\n\tp.tags = p.tags[:n-1]\n\treturn nil\n}\n\n// marshalTextInterface marshals a TextMarshaler interface value.\nfunc (p *printer) marshalTextInterface(val encoding.TextMarshaler, start StartElement) error {\n\tif err := p.writeStart(&start); err != nil {\n\t\treturn err\n\t}\n\ttext, err := val.MarshalText()\n\tif err != nil {\n\t\treturn err\n\t}\n\tEscapeText(p, text)\n\treturn p.writeEnd(start.Name)\n}\n\n// writeStart writes the given start element.\nfunc (p *printer) writeStart(start *StartElement) error {\n\tif start.Name.Local == \"\" {\n\t\treturn fmt.Errorf(\"xml: start tag with no name\")\n\t}\n\n\tp.tags = append(p.tags, start.Name)\n\tp.markPrefix()\n\t// Define any name spaces explicitly declared in the attributes.\n\t// We do this as a separate pass so that explicitly declared prefixes\n\t// will take precedence over implicitly declared prefixes\n\t// regardless of the order of the attributes.\n\tignoreNonEmptyDefault := start.Name.Space == \"\"\n\tfor _, attr := range start.Attr {\n\t\tif err := p.defineNS(attr, ignoreNonEmptyDefault); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Define any new name spaces implied by the attributes.\n\tfor _, attr := range start.Attr {\n\t\tname := attr.Name\n\t\t// From http://www.w3.org/TR/xml-names11/#defaulting\n\t\t// \"Default namespace declarations do not apply directly\n\t\t// to attribute names; the interpretation of unprefixed\n\t\t// attributes is determined by the element on which they\n\t\t// appear.\"\n\t\t// This means we don't need to create a new namespace\n\t\t// when an attribute name space is empty.\n\t\tif name.Space != \"\" && !name.isNamespace() {\n\t\t\tp.createNSPrefix(name.Space, true)\n\t\t}\n\t}\n\tp.createNSPrefix(start.Name.Space, false)\n\n\tp.writeIndent(1)\n\tp.WriteByte('<')\n\tp.writeName(start.Name, false)\n\tp.writeNamespaces()\n\tfor _, attr := range start.Attr {\n\t\tname := attr.Name\n\t\tif name.Local == \"\" || name.isNamespace() {\n\t\t\t// Namespaces have already been written by writeNamespaces above.\n\t\t\tcontinue\n\t\t}\n\t\tp.WriteByte(' ')\n\t\tp.writeName(name, true)\n\t\tp.WriteString(`=\"`)\n\t\tp.EscapeString(attr.Value)\n\t\tp.WriteByte('\"')\n\t}\n\tp.WriteByte('>')\n\treturn nil\n}\n\n// writeName writes the given name. It assumes\n// that p.createNSPrefix(name) has already been called.\nfunc (p *printer) writeName(name Name, isAttr bool) {\n\tif prefix := p.prefixForNS(name.Space, isAttr); prefix != \"\" {\n\t\tp.WriteString(prefix)\n\t\tp.WriteByte(':')\n\t}\n\tp.WriteString(name.Local)\n}\n\nfunc (p *printer) writeEnd(name Name) error {\n\tif name.Local == \"\" {\n\t\treturn fmt.Errorf(\"xml: end tag with no name\")\n\t}\n\tif len(p.tags) == 0 || p.tags[len(p.tags)-1].Local == \"\" {\n\t\treturn fmt.Errorf(\"xml: end tag </%s> without start tag\", name.Local)\n\t}\n\tif top := p.tags[len(p.tags)-1]; top != name {\n\t\tif top.Local != name.Local {\n\t\t\treturn fmt.Errorf(\"xml: end tag </%s> does not match start tag <%s>\", name.Local, top.Local)\n\t\t}\n\t\treturn fmt.Errorf(\"xml: end tag </%s> in namespace %s does not match start tag <%s> in namespace %s\", name.Local, name.Space, top.Local, top.Space)\n\t}\n\tp.tags = p.tags[:len(p.tags)-1]\n\n\tp.writeIndent(-1)\n\tp.WriteByte('<')\n\tp.WriteByte('/')\n\tp.writeName(name, false)\n\tp.WriteByte('>')\n\tp.popPrefix()\n\treturn nil\n}\n\nfunc (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) (string, []byte, error) {\n\tswitch val.Kind() {\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn strconv.FormatInt(val.Int(), 10), nil, nil\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\treturn strconv.FormatUint(val.Uint(), 10), nil, nil\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn strconv.FormatFloat(val.Float(), 'g', -1, val.Type().Bits()), nil, nil\n\tcase reflect.String:\n\t\treturn val.String(), nil, nil\n\tcase reflect.Bool:\n\t\treturn strconv.FormatBool(val.Bool()), nil, nil\n\tcase reflect.Array:\n\t\tif typ.Elem().Kind() != reflect.Uint8 {\n\t\t\tbreak\n\t\t}\n\t\t// [...]byte\n\t\tvar bytes []byte\n\t\tif val.CanAddr() {\n\t\t\tbytes = val.Slice(0, val.Len()).Bytes()\n\t\t} else {\n\t\t\tbytes = make([]byte, val.Len())\n\t\t\treflect.Copy(reflect.ValueOf(bytes), val)\n\t\t}\n\t\treturn \"\", bytes, nil\n\tcase reflect.Slice:\n\t\tif typ.Elem().Kind() != reflect.Uint8 {\n\t\t\tbreak\n\t\t}\n\t\t// []byte\n\t\treturn \"\", val.Bytes(), nil\n\t}\n\treturn \"\", nil, &UnsupportedTypeError{typ}\n}\n\nvar ddBytes = []byte(\"--\")\n\nfunc (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {\n\ts := parentStack{p: p}\n\tfor i := range tinfo.fields {\n\t\tfinfo := &tinfo.fields[i]\n\t\tif finfo.flags&fAttr != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvf := finfo.value(val)\n\n\t\t// Dereference or skip nil pointer, interface values.\n\t\tswitch vf.Kind() {\n\t\tcase reflect.Ptr, reflect.Interface:\n\t\t\tif !vf.IsNil() {\n\t\t\t\tvf = vf.Elem()\n\t\t\t}\n\t\t}\n\n\t\tswitch finfo.flags & fMode {\n\t\tcase fCharData:\n\t\t\tif err := s.setParents(&noField, reflect.Value{}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif vf.CanInterface() && vf.Type().Implements(textMarshalerType) {\n\t\t\t\tdata, err := vf.Interface().(encoding.TextMarshaler).MarshalText()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tEscape(p, data)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif vf.CanAddr() {\n\t\t\t\tpv := vf.Addr()\n\t\t\t\tif pv.CanInterface() && pv.Type().Implements(textMarshalerType) {\n\t\t\t\t\tdata, err := pv.Interface().(encoding.TextMarshaler).MarshalText()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tEscape(p, data)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tvar scratch [64]byte\n\t\t\tswitch vf.Kind() {\n\t\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\t\t\tEscape(p, strconv.AppendInt(scratch[:0], vf.Int(), 10))\n\t\t\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\t\t\tEscape(p, strconv.AppendUint(scratch[:0], vf.Uint(), 10))\n\t\t\tcase reflect.Float32, reflect.Float64:\n\t\t\t\tEscape(p, strconv.AppendFloat(scratch[:0], vf.Float(), 'g', -1, vf.Type().Bits()))\n\t\t\tcase reflect.Bool:\n\t\t\t\tEscape(p, strconv.AppendBool(scratch[:0], vf.Bool()))\n\t\t\tcase reflect.String:\n\t\t\t\tif err := EscapeText(p, []byte(vf.String())); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase reflect.Slice:\n\t\t\t\tif elem, ok := vf.Interface().([]byte); ok {\n\t\t\t\t\tif err := EscapeText(p, elem); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\n\t\tcase fComment:\n\t\t\tif err := s.setParents(&noField, reflect.Value{}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tk := vf.Kind()\n\t\t\tif !(k == reflect.String || k == reflect.Slice && vf.Type().Elem().Kind() == reflect.Uint8) {\n\t\t\t\treturn fmt.Errorf(\"xml: bad type for comment field of %s\", val.Type())\n\t\t\t}\n\t\t\tif vf.Len() == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp.writeIndent(0)\n\t\t\tp.WriteString(\"<!--\")\n\t\t\tdashDash := false\n\t\t\tdashLast := false\n\t\t\tswitch k {\n\t\t\tcase reflect.String:\n\t\t\t\ts := vf.String()\n\t\t\t\tdashDash = strings.Index(s, \"--\") >= 0\n\t\t\t\tdashLast = s[len(s)-1] == '-'\n\t\t\t\tif !dashDash {\n\t\t\t\t\tp.WriteString(s)\n\t\t\t\t}\n\t\t\tcase reflect.Slice:\n\t\t\t\tb := vf.Bytes()\n\t\t\t\tdashDash = bytes.Index(b, ddBytes) >= 0\n\t\t\t\tdashLast = b[len(b)-1] == '-'\n\t\t\t\tif !dashDash {\n\t\t\t\t\tp.Write(b)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tpanic(\"can't happen\")\n\t\t\t}\n\t\t\tif dashDash {\n\t\t\t\treturn fmt.Errorf(`xml: comments must not contain \"--\"`)\n\t\t\t}\n\t\t\tif dashLast {\n\t\t\t\t// \"--->\" is invalid grammar. Make it \"- -->\"\n\t\t\t\tp.WriteByte(' ')\n\t\t\t}\n\t\t\tp.WriteString(\"-->\")\n\t\t\tcontinue\n\n\t\tcase fInnerXml:\n\t\t\tiface := vf.Interface()\n\t\t\tswitch raw := iface.(type) {\n\t\t\tcase []byte:\n\t\t\t\tp.Write(raw)\n\t\t\t\tcontinue\n\t\t\tcase string:\n\t\t\t\tp.WriteString(raw)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\tcase fElement, fElement | fAny:\n\t\t\tif err := s.setParents(finfo, vf); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := p.marshalValue(vf, finfo, nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := s.setParents(&noField, reflect.Value{}); err != nil {\n\t\treturn err\n\t}\n\treturn p.cachedWriteError()\n}\n\nvar noField fieldInfo\n\n// return the bufio Writer's cached write error\nfunc (p *printer) cachedWriteError() error {\n\t_, err := p.Write(nil)\n\treturn err\n}\n\nfunc (p *printer) writeIndent(depthDelta int) {\n\tif len(p.prefix) == 0 && len(p.indent) == 0 {\n\t\treturn\n\t}\n\tif depthDelta < 0 {\n\t\tp.depth--\n\t\tif p.indentedIn {\n\t\t\tp.indentedIn = false\n\t\t\treturn\n\t\t}\n\t\tp.indentedIn = false\n\t}\n\tif p.putNewline {\n\t\tp.WriteByte('\\n')\n\t} else {\n\t\tp.putNewline = true\n\t}\n\tif len(p.prefix) > 0 {\n\t\tp.WriteString(p.prefix)\n\t}\n\tif len(p.indent) > 0 {\n\t\tfor i := 0; i < p.depth; i++ {\n\t\t\tp.WriteString(p.indent)\n\t\t}\n\t}\n\tif depthDelta > 0 {\n\t\tp.depth++\n\t\tp.indentedIn = true\n\t}\n}\n\ntype parentStack struct {\n\tp       *printer\n\txmlns   string\n\tparents []string\n}\n\n// setParents sets the stack of current parents to those found in finfo.\n// It only writes the start elements if vf holds a non-nil value.\n// If finfo is &noField, it pops all elements.\nfunc (s *parentStack) setParents(finfo *fieldInfo, vf reflect.Value) error {\n\txmlns := s.p.defaultNS\n\tif finfo.xmlns != \"\" {\n\t\txmlns = finfo.xmlns\n\t}\n\tcommonParents := 0\n\tif xmlns == s.xmlns {\n\t\tfor ; commonParents < len(finfo.parents) && commonParents < len(s.parents); commonParents++ {\n\t\t\tif finfo.parents[commonParents] != s.parents[commonParents] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\t// Pop off any parents that aren't in common with the previous field.\n\tfor i := len(s.parents) - 1; i >= commonParents; i-- {\n\t\tif err := s.p.writeEnd(Name{\n\t\t\tSpace: s.xmlns,\n\t\t\tLocal: s.parents[i],\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\ts.parents = finfo.parents\n\ts.xmlns = xmlns\n\tif commonParents >= len(s.parents) {\n\t\t// No new elements to push.\n\t\treturn nil\n\t}\n\tif (vf.Kind() == reflect.Ptr || vf.Kind() == reflect.Interface) && vf.IsNil() {\n\t\t// The element is nil, so no need for the start elements.\n\t\ts.parents = s.parents[:commonParents]\n\t\treturn nil\n\t}\n\t// Push any new parents required.\n\tfor _, name := range s.parents[commonParents:] {\n\t\tstart := &StartElement{\n\t\t\tName: Name{\n\t\t\t\tSpace: s.xmlns,\n\t\t\t\tLocal: name,\n\t\t\t},\n\t\t}\n\t\t// Set the default name space for parent elements\n\t\t// to match what we do with other elements.\n\t\tif s.xmlns != s.p.defaultNS {\n\t\t\tstart.setDefaultNamespace()\n\t\t}\n\t\tif err := s.p.writeStart(start); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// A MarshalXMLError is returned when Marshal encounters a type\n// that cannot be converted into XML.\ntype UnsupportedTypeError struct {\n\tType reflect.Type\n}\n\nfunc (e *UnsupportedTypeError) Error() string {\n\treturn \"xml: unsupported type: \" + e.Type.String()\n}\n\nfunc isEmptyValue(v reflect.Value) bool {\n\tswitch v.Kind() {\n\tcase reflect.Array, reflect.Map, reflect.Slice, reflect.String:\n\t\treturn v.Len() == 0\n\tcase reflect.Bool:\n\t\treturn !v.Bool()\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn v.Int() == 0\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\treturn v.Uint() == 0\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn v.Float() == 0\n\tcase reflect.Interface, reflect.Ptr:\n\t\treturn v.IsNil()\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/marshal_test.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype DriveType int\n\nconst (\n\tHyperDrive DriveType = iota\n\tImprobabilityDrive\n)\n\ntype Passenger struct {\n\tName   []string `xml:\"name\"`\n\tWeight float32  `xml:\"weight\"`\n}\n\ntype Ship struct {\n\tXMLName struct{} `xml:\"spaceship\"`\n\n\tName      string       `xml:\"name,attr\"`\n\tPilot     string       `xml:\"pilot,attr\"`\n\tDrive     DriveType    `xml:\"drive\"`\n\tAge       uint         `xml:\"age\"`\n\tPassenger []*Passenger `xml:\"passenger\"`\n\tsecret    string\n}\n\ntype NamedType string\n\ntype Port struct {\n\tXMLName struct{} `xml:\"port\"`\n\tType    string   `xml:\"type,attr,omitempty\"`\n\tComment string   `xml:\",comment\"`\n\tNumber  string   `xml:\",chardata\"`\n}\n\ntype Domain struct {\n\tXMLName struct{} `xml:\"domain\"`\n\tCountry string   `xml:\",attr,omitempty\"`\n\tName    []byte   `xml:\",chardata\"`\n\tComment []byte   `xml:\",comment\"`\n}\n\ntype Book struct {\n\tXMLName struct{} `xml:\"book\"`\n\tTitle   string   `xml:\",chardata\"`\n}\n\ntype Event struct {\n\tXMLName struct{} `xml:\"event\"`\n\tYear    int      `xml:\",chardata\"`\n}\n\ntype Movie struct {\n\tXMLName struct{} `xml:\"movie\"`\n\tLength  uint     `xml:\",chardata\"`\n}\n\ntype Pi struct {\n\tXMLName       struct{} `xml:\"pi\"`\n\tApproximation float32  `xml:\",chardata\"`\n}\n\ntype Universe struct {\n\tXMLName struct{} `xml:\"universe\"`\n\tVisible float64  `xml:\",chardata\"`\n}\n\ntype Particle struct {\n\tXMLName struct{} `xml:\"particle\"`\n\tHasMass bool     `xml:\",chardata\"`\n}\n\ntype Departure struct {\n\tXMLName struct{}  `xml:\"departure\"`\n\tWhen    time.Time `xml:\",chardata\"`\n}\n\ntype SecretAgent struct {\n\tXMLName   struct{} `xml:\"agent\"`\n\tHandle    string   `xml:\"handle,attr\"`\n\tIdentity  string\n\tObfuscate string `xml:\",innerxml\"`\n}\n\ntype NestedItems struct {\n\tXMLName struct{} `xml:\"result\"`\n\tItems   []string `xml:\">item\"`\n\tItem1   []string `xml:\"Items>item1\"`\n}\n\ntype NestedOrder struct {\n\tXMLName struct{} `xml:\"result\"`\n\tField1  string   `xml:\"parent>c\"`\n\tField2  string   `xml:\"parent>b\"`\n\tField3  string   `xml:\"parent>a\"`\n}\n\ntype MixedNested struct {\n\tXMLName struct{} `xml:\"result\"`\n\tA       string   `xml:\"parent1>a\"`\n\tB       string   `xml:\"b\"`\n\tC       string   `xml:\"parent1>parent2>c\"`\n\tD       string   `xml:\"parent1>d\"`\n}\n\ntype NilTest struct {\n\tA interface{} `xml:\"parent1>parent2>a\"`\n\tB interface{} `xml:\"parent1>b\"`\n\tC interface{} `xml:\"parent1>parent2>c\"`\n}\n\ntype Service struct {\n\tXMLName struct{} `xml:\"service\"`\n\tDomain  *Domain  `xml:\"host>domain\"`\n\tPort    *Port    `xml:\"host>port\"`\n\tExtra1  interface{}\n\tExtra2  interface{} `xml:\"host>extra2\"`\n}\n\nvar nilStruct *Ship\n\ntype EmbedA struct {\n\tEmbedC\n\tEmbedB EmbedB\n\tFieldA string\n}\n\ntype EmbedB struct {\n\tFieldB string\n\t*EmbedC\n}\n\ntype EmbedC struct {\n\tFieldA1 string `xml:\"FieldA>A1\"`\n\tFieldA2 string `xml:\"FieldA>A2\"`\n\tFieldB  string\n\tFieldC  string\n}\n\ntype NameCasing struct {\n\tXMLName struct{} `xml:\"casing\"`\n\tXy      string\n\tXY      string\n\tXyA     string `xml:\"Xy,attr\"`\n\tXYA     string `xml:\"XY,attr\"`\n}\n\ntype NamePrecedence struct {\n\tXMLName     Name              `xml:\"Parent\"`\n\tFromTag     XMLNameWithoutTag `xml:\"InTag\"`\n\tFromNameVal XMLNameWithoutTag\n\tFromNameTag XMLNameWithTag\n\tInFieldName string\n}\n\ntype XMLNameWithTag struct {\n\tXMLName Name   `xml:\"InXMLNameTag\"`\n\tValue   string `xml:\",chardata\"`\n}\n\ntype XMLNameWithNSTag struct {\n\tXMLName Name   `xml:\"ns InXMLNameWithNSTag\"`\n\tValue   string `xml:\",chardata\"`\n}\n\ntype XMLNameWithoutTag struct {\n\tXMLName Name\n\tValue   string `xml:\",chardata\"`\n}\n\ntype NameInField struct {\n\tFoo Name `xml:\"ns foo\"`\n}\n\ntype AttrTest struct {\n\tInt   int     `xml:\",attr\"`\n\tNamed int     `xml:\"int,attr\"`\n\tFloat float64 `xml:\",attr\"`\n\tUint8 uint8   `xml:\",attr\"`\n\tBool  bool    `xml:\",attr\"`\n\tStr   string  `xml:\",attr\"`\n\tBytes []byte  `xml:\",attr\"`\n}\n\ntype OmitAttrTest struct {\n\tInt   int     `xml:\",attr,omitempty\"`\n\tNamed int     `xml:\"int,attr,omitempty\"`\n\tFloat float64 `xml:\",attr,omitempty\"`\n\tUint8 uint8   `xml:\",attr,omitempty\"`\n\tBool  bool    `xml:\",attr,omitempty\"`\n\tStr   string  `xml:\",attr,omitempty\"`\n\tBytes []byte  `xml:\",attr,omitempty\"`\n}\n\ntype OmitFieldTest struct {\n\tInt   int           `xml:\",omitempty\"`\n\tNamed int           `xml:\"int,omitempty\"`\n\tFloat float64       `xml:\",omitempty\"`\n\tUint8 uint8         `xml:\",omitempty\"`\n\tBool  bool          `xml:\",omitempty\"`\n\tStr   string        `xml:\",omitempty\"`\n\tBytes []byte        `xml:\",omitempty\"`\n\tPtr   *PresenceTest `xml:\",omitempty\"`\n}\n\ntype AnyTest struct {\n\tXMLName  struct{}  `xml:\"a\"`\n\tNested   string    `xml:\"nested>value\"`\n\tAnyField AnyHolder `xml:\",any\"`\n}\n\ntype AnyOmitTest struct {\n\tXMLName  struct{}   `xml:\"a\"`\n\tNested   string     `xml:\"nested>value\"`\n\tAnyField *AnyHolder `xml:\",any,omitempty\"`\n}\n\ntype AnySliceTest struct {\n\tXMLName  struct{}    `xml:\"a\"`\n\tNested   string      `xml:\"nested>value\"`\n\tAnyField []AnyHolder `xml:\",any\"`\n}\n\ntype AnyHolder struct {\n\tXMLName Name\n\tXML     string `xml:\",innerxml\"`\n}\n\ntype RecurseA struct {\n\tA string\n\tB *RecurseB\n}\n\ntype RecurseB struct {\n\tA *RecurseA\n\tB string\n}\n\ntype PresenceTest struct {\n\tExists *struct{}\n}\n\ntype IgnoreTest struct {\n\tPublicSecret string `xml:\"-\"`\n}\n\ntype MyBytes []byte\n\ntype Data struct {\n\tBytes  []byte\n\tAttr   []byte `xml:\",attr\"`\n\tCustom MyBytes\n}\n\ntype Plain struct {\n\tV interface{}\n}\n\ntype MyInt int\n\ntype EmbedInt struct {\n\tMyInt\n}\n\ntype Strings struct {\n\tX []string `xml:\"A>B,omitempty\"`\n}\n\ntype PointerFieldsTest struct {\n\tXMLName  Name    `xml:\"dummy\"`\n\tName     *string `xml:\"name,attr\"`\n\tAge      *uint   `xml:\"age,attr\"`\n\tEmpty    *string `xml:\"empty,attr\"`\n\tContents *string `xml:\",chardata\"`\n}\n\ntype ChardataEmptyTest struct {\n\tXMLName  Name    `xml:\"test\"`\n\tContents *string `xml:\",chardata\"`\n}\n\ntype MyMarshalerTest struct {\n}\n\nvar _ Marshaler = (*MyMarshalerTest)(nil)\n\nfunc (m *MyMarshalerTest) MarshalXML(e *Encoder, start StartElement) error {\n\te.EncodeToken(start)\n\te.EncodeToken(CharData([]byte(\"hello world\")))\n\te.EncodeToken(EndElement{start.Name})\n\treturn nil\n}\n\ntype MyMarshalerAttrTest struct{}\n\nvar _ MarshalerAttr = (*MyMarshalerAttrTest)(nil)\n\nfunc (m *MyMarshalerAttrTest) MarshalXMLAttr(name Name) (Attr, error) {\n\treturn Attr{name, \"hello world\"}, nil\n}\n\ntype MyMarshalerValueAttrTest struct{}\n\nvar _ MarshalerAttr = MyMarshalerValueAttrTest{}\n\nfunc (m MyMarshalerValueAttrTest) MarshalXMLAttr(name Name) (Attr, error) {\n\treturn Attr{name, \"hello world\"}, nil\n}\n\ntype MarshalerStruct struct {\n\tFoo MyMarshalerAttrTest `xml:\",attr\"`\n}\n\ntype MarshalerValueStruct struct {\n\tFoo MyMarshalerValueAttrTest `xml:\",attr\"`\n}\n\ntype InnerStruct struct {\n\tXMLName Name `xml:\"testns outer\"`\n}\n\ntype OuterStruct struct {\n\tInnerStruct\n\tIntAttr int `xml:\"int,attr\"`\n}\n\ntype OuterNamedStruct struct {\n\tInnerStruct\n\tXMLName Name `xml:\"outerns test\"`\n\tIntAttr int  `xml:\"int,attr\"`\n}\n\ntype OuterNamedOrderedStruct struct {\n\tXMLName Name `xml:\"outerns test\"`\n\tInnerStruct\n\tIntAttr int `xml:\"int,attr\"`\n}\n\ntype OuterOuterStruct struct {\n\tOuterStruct\n}\n\ntype NestedAndChardata struct {\n\tAB       []string `xml:\"A>B\"`\n\tChardata string   `xml:\",chardata\"`\n}\n\ntype NestedAndComment struct {\n\tAB      []string `xml:\"A>B\"`\n\tComment string   `xml:\",comment\"`\n}\n\ntype XMLNSFieldStruct struct {\n\tNs   string `xml:\"xmlns,attr\"`\n\tBody string\n}\n\ntype NamedXMLNSFieldStruct struct {\n\tXMLName struct{} `xml:\"testns test\"`\n\tNs      string   `xml:\"xmlns,attr\"`\n\tBody    string\n}\n\ntype XMLNSFieldStructWithOmitEmpty struct {\n\tNs   string `xml:\"xmlns,attr,omitempty\"`\n\tBody string\n}\n\ntype NamedXMLNSFieldStructWithEmptyNamespace struct {\n\tXMLName struct{} `xml:\"test\"`\n\tNs      string   `xml:\"xmlns,attr\"`\n\tBody    string\n}\n\ntype RecursiveXMLNSFieldStruct struct {\n\tNs   string                     `xml:\"xmlns,attr\"`\n\tBody *RecursiveXMLNSFieldStruct `xml:\",omitempty\"`\n\tText string                     `xml:\",omitempty\"`\n}\n\nfunc ifaceptr(x interface{}) interface{} {\n\treturn &x\n}\n\nvar (\n\tnameAttr     = \"Sarah\"\n\tageAttr      = uint(12)\n\tcontentsAttr = \"lorem ipsum\"\n)\n\n// Unless explicitly stated as such (or *Plain), all of the\n// tests below are two-way tests. When introducing new tests,\n// please try to make them two-way as well to ensure that\n// marshalling and unmarshalling are as symmetrical as feasible.\nvar marshalTests = []struct {\n\tValue         interface{}\n\tExpectXML     string\n\tMarshalOnly   bool\n\tUnmarshalOnly bool\n}{\n\t// Test nil marshals to nothing\n\t{Value: nil, ExpectXML: ``, MarshalOnly: true},\n\t{Value: nilStruct, ExpectXML: ``, MarshalOnly: true},\n\n\t// Test value types\n\t{Value: &Plain{true}, ExpectXML: `<Plain><V>true</V></Plain>`},\n\t{Value: &Plain{false}, ExpectXML: `<Plain><V>false</V></Plain>`},\n\t{Value: &Plain{int(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{int8(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{int16(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{int32(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{uint(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{uint8(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{uint16(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{uint32(42)}, ExpectXML: `<Plain><V>42</V></Plain>`},\n\t{Value: &Plain{float32(1.25)}, ExpectXML: `<Plain><V>1.25</V></Plain>`},\n\t{Value: &Plain{float64(1.25)}, ExpectXML: `<Plain><V>1.25</V></Plain>`},\n\t{Value: &Plain{uintptr(0xFFDD)}, ExpectXML: `<Plain><V>65501</V></Plain>`},\n\t{Value: &Plain{\"gopher\"}, ExpectXML: `<Plain><V>gopher</V></Plain>`},\n\t{Value: &Plain{[]byte(\"gopher\")}, ExpectXML: `<Plain><V>gopher</V></Plain>`},\n\t{Value: &Plain{\"</>\"}, ExpectXML: `<Plain><V>&lt;/&gt;</V></Plain>`},\n\t{Value: &Plain{[]byte(\"</>\")}, ExpectXML: `<Plain><V>&lt;/&gt;</V></Plain>`},\n\t{Value: &Plain{[3]byte{'<', '/', '>'}}, ExpectXML: `<Plain><V>&lt;/&gt;</V></Plain>`},\n\t{Value: &Plain{NamedType(\"potato\")}, ExpectXML: `<Plain><V>potato</V></Plain>`},\n\t{Value: &Plain{[]int{1, 2, 3}}, ExpectXML: `<Plain><V>1</V><V>2</V><V>3</V></Plain>`},\n\t{Value: &Plain{[3]int{1, 2, 3}}, ExpectXML: `<Plain><V>1</V><V>2</V><V>3</V></Plain>`},\n\t{Value: ifaceptr(true), MarshalOnly: true, ExpectXML: `<bool>true</bool>`},\n\n\t// Test time.\n\t{\n\t\tValue:     &Plain{time.Unix(1e9, 123456789).UTC()},\n\t\tExpectXML: `<Plain><V>2001-09-09T01:46:40.123456789Z</V></Plain>`,\n\t},\n\n\t// A pointer to struct{} may be used to test for an element's presence.\n\t{\n\t\tValue:     &PresenceTest{new(struct{})},\n\t\tExpectXML: `<PresenceTest><Exists></Exists></PresenceTest>`,\n\t},\n\t{\n\t\tValue:     &PresenceTest{},\n\t\tExpectXML: `<PresenceTest></PresenceTest>`,\n\t},\n\n\t// A pointer to struct{} may be used to test for an element's presence.\n\t{\n\t\tValue:     &PresenceTest{new(struct{})},\n\t\tExpectXML: `<PresenceTest><Exists></Exists></PresenceTest>`,\n\t},\n\t{\n\t\tValue:     &PresenceTest{},\n\t\tExpectXML: `<PresenceTest></PresenceTest>`,\n\t},\n\n\t// A []byte field is only nil if the element was not found.\n\t{\n\t\tValue:         &Data{},\n\t\tExpectXML:     `<Data></Data>`,\n\t\tUnmarshalOnly: true,\n\t},\n\t{\n\t\tValue:         &Data{Bytes: []byte{}, Custom: MyBytes{}, Attr: []byte{}},\n\t\tExpectXML:     `<Data Attr=\"\"><Bytes></Bytes><Custom></Custom></Data>`,\n\t\tUnmarshalOnly: true,\n\t},\n\n\t// Check that []byte works, including named []byte types.\n\t{\n\t\tValue:     &Data{Bytes: []byte(\"ab\"), Custom: MyBytes(\"cd\"), Attr: []byte{'v'}},\n\t\tExpectXML: `<Data Attr=\"v\"><Bytes>ab</Bytes><Custom>cd</Custom></Data>`,\n\t},\n\n\t// Test innerxml\n\t{\n\t\tValue: &SecretAgent{\n\t\t\tHandle:    \"007\",\n\t\t\tIdentity:  \"James Bond\",\n\t\t\tObfuscate: \"<redacted/>\",\n\t\t},\n\t\tExpectXML:   `<agent handle=\"007\"><Identity>James Bond</Identity><redacted/></agent>`,\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tValue: &SecretAgent{\n\t\t\tHandle:    \"007\",\n\t\t\tIdentity:  \"James Bond\",\n\t\t\tObfuscate: \"<Identity>James Bond</Identity><redacted/>\",\n\t\t},\n\t\tExpectXML:     `<agent handle=\"007\"><Identity>James Bond</Identity><redacted/></agent>`,\n\t\tUnmarshalOnly: true,\n\t},\n\n\t// Test structs\n\t{Value: &Port{Type: \"ssl\", Number: \"443\"}, ExpectXML: `<port type=\"ssl\">443</port>`},\n\t{Value: &Port{Number: \"443\"}, ExpectXML: `<port>443</port>`},\n\t{Value: &Port{Type: \"<unix>\"}, ExpectXML: `<port type=\"&lt;unix&gt;\"></port>`},\n\t{Value: &Port{Number: \"443\", Comment: \"https\"}, ExpectXML: `<port><!--https-->443</port>`},\n\t{Value: &Port{Number: \"443\", Comment: \"add space-\"}, ExpectXML: `<port><!--add space- -->443</port>`, MarshalOnly: true},\n\t{Value: &Domain{Name: []byte(\"google.com&friends\")}, ExpectXML: `<domain>google.com&amp;friends</domain>`},\n\t{Value: &Domain{Name: []byte(\"google.com\"), Comment: []byte(\" &friends \")}, ExpectXML: `<domain>google.com<!-- &friends --></domain>`},\n\t{Value: &Book{Title: \"Pride & Prejudice\"}, ExpectXML: `<book>Pride &amp; Prejudice</book>`},\n\t{Value: &Event{Year: -3114}, ExpectXML: `<event>-3114</event>`},\n\t{Value: &Movie{Length: 13440}, ExpectXML: `<movie>13440</movie>`},\n\t{Value: &Pi{Approximation: 3.14159265}, ExpectXML: `<pi>3.1415927</pi>`},\n\t{Value: &Universe{Visible: 9.3e13}, ExpectXML: `<universe>9.3e+13</universe>`},\n\t{Value: &Particle{HasMass: true}, ExpectXML: `<particle>true</particle>`},\n\t{Value: &Departure{When: ParseTime(\"2013-01-09T00:15:00-09:00\")}, ExpectXML: `<departure>2013-01-09T00:15:00-09:00</departure>`},\n\t{Value: atomValue, ExpectXML: atomXml},\n\t{\n\t\tValue: &Ship{\n\t\t\tName:  \"Heart of Gold\",\n\t\t\tPilot: \"Computer\",\n\t\t\tAge:   1,\n\t\t\tDrive: ImprobabilityDrive,\n\t\t\tPassenger: []*Passenger{\n\t\t\t\t{\n\t\t\t\t\tName:   []string{\"Zaphod\", \"Beeblebrox\"},\n\t\t\t\t\tWeight: 7.25,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:   []string{\"Trisha\", \"McMillen\"},\n\t\t\t\t\tWeight: 5.5,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:   []string{\"Ford\", \"Prefect\"},\n\t\t\t\t\tWeight: 7,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:   []string{\"Arthur\", \"Dent\"},\n\t\t\t\t\tWeight: 6.75,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tExpectXML: `<spaceship name=\"Heart of Gold\" pilot=\"Computer\">` +\n\t\t\t`<drive>` + strconv.Itoa(int(ImprobabilityDrive)) + `</drive>` +\n\t\t\t`<age>1</age>` +\n\t\t\t`<passenger>` +\n\t\t\t`<name>Zaphod</name>` +\n\t\t\t`<name>Beeblebrox</name>` +\n\t\t\t`<weight>7.25</weight>` +\n\t\t\t`</passenger>` +\n\t\t\t`<passenger>` +\n\t\t\t`<name>Trisha</name>` +\n\t\t\t`<name>McMillen</name>` +\n\t\t\t`<weight>5.5</weight>` +\n\t\t\t`</passenger>` +\n\t\t\t`<passenger>` +\n\t\t\t`<name>Ford</name>` +\n\t\t\t`<name>Prefect</name>` +\n\t\t\t`<weight>7</weight>` +\n\t\t\t`</passenger>` +\n\t\t\t`<passenger>` +\n\t\t\t`<name>Arthur</name>` +\n\t\t\t`<name>Dent</name>` +\n\t\t\t`<weight>6.75</weight>` +\n\t\t\t`</passenger>` +\n\t\t\t`</spaceship>`,\n\t},\n\n\t// Test a>b\n\t{\n\t\tValue: &NestedItems{Items: nil, Item1: nil},\n\t\tExpectXML: `<result>` +\n\t\t\t`<Items>` +\n\t\t\t`</Items>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue: &NestedItems{Items: []string{}, Item1: []string{}},\n\t\tExpectXML: `<result>` +\n\t\t\t`<Items>` +\n\t\t\t`</Items>` +\n\t\t\t`</result>`,\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tValue: &NestedItems{Items: nil, Item1: []string{\"A\"}},\n\t\tExpectXML: `<result>` +\n\t\t\t`<Items>` +\n\t\t\t`<item1>A</item1>` +\n\t\t\t`</Items>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue: &NestedItems{Items: []string{\"A\", \"B\"}, Item1: nil},\n\t\tExpectXML: `<result>` +\n\t\t\t`<Items>` +\n\t\t\t`<item>A</item>` +\n\t\t\t`<item>B</item>` +\n\t\t\t`</Items>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue: &NestedItems{Items: []string{\"A\", \"B\"}, Item1: []string{\"C\"}},\n\t\tExpectXML: `<result>` +\n\t\t\t`<Items>` +\n\t\t\t`<item>A</item>` +\n\t\t\t`<item>B</item>` +\n\t\t\t`<item1>C</item1>` +\n\t\t\t`</Items>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue: &NestedOrder{Field1: \"C\", Field2: \"B\", Field3: \"A\"},\n\t\tExpectXML: `<result>` +\n\t\t\t`<parent>` +\n\t\t\t`<c>C</c>` +\n\t\t\t`<b>B</b>` +\n\t\t\t`<a>A</a>` +\n\t\t\t`</parent>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue: &NilTest{A: \"A\", B: nil, C: \"C\"},\n\t\tExpectXML: `<NilTest>` +\n\t\t\t`<parent1>` +\n\t\t\t`<parent2><a>A</a></parent2>` +\n\t\t\t`<parent2><c>C</c></parent2>` +\n\t\t\t`</parent1>` +\n\t\t\t`</NilTest>`,\n\t\tMarshalOnly: true, // Uses interface{}\n\t},\n\t{\n\t\tValue: &MixedNested{A: \"A\", B: \"B\", C: \"C\", D: \"D\"},\n\t\tExpectXML: `<result>` +\n\t\t\t`<parent1><a>A</a></parent1>` +\n\t\t\t`<b>B</b>` +\n\t\t\t`<parent1>` +\n\t\t\t`<parent2><c>C</c></parent2>` +\n\t\t\t`<d>D</d>` +\n\t\t\t`</parent1>` +\n\t\t\t`</result>`,\n\t},\n\t{\n\t\tValue:     &Service{Port: &Port{Number: \"80\"}},\n\t\tExpectXML: `<service><host><port>80</port></host></service>`,\n\t},\n\t{\n\t\tValue:     &Service{},\n\t\tExpectXML: `<service></service>`,\n\t},\n\t{\n\t\tValue: &Service{Port: &Port{Number: \"80\"}, Extra1: \"A\", Extra2: \"B\"},\n\t\tExpectXML: `<service>` +\n\t\t\t`<host><port>80</port></host>` +\n\t\t\t`<Extra1>A</Extra1>` +\n\t\t\t`<host><extra2>B</extra2></host>` +\n\t\t\t`</service>`,\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tValue: &Service{Port: &Port{Number: \"80\"}, Extra2: \"example\"},\n\t\tExpectXML: `<service>` +\n\t\t\t`<host><port>80</port></host>` +\n\t\t\t`<host><extra2>example</extra2></host>` +\n\t\t\t`</service>`,\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tValue: &struct {\n\t\t\tXMLName struct{} `xml:\"space top\"`\n\t\t\tA       string   `xml:\"x>a\"`\n\t\t\tB       string   `xml:\"x>b\"`\n\t\t\tC       string   `xml:\"space x>c\"`\n\t\t\tC1      string   `xml:\"space1 x>c\"`\n\t\t\tD1      string   `xml:\"space1 x>d\"`\n\t\t\tE1      string   `xml:\"x>e\"`\n\t\t}{\n\t\t\tA:  \"a\",\n\t\t\tB:  \"b\",\n\t\t\tC:  \"c\",\n\t\t\tC1: \"c1\",\n\t\t\tD1: \"d1\",\n\t\t\tE1: \"e1\",\n\t\t},\n\t\tExpectXML: `<top xmlns=\"space\">` +\n\t\t\t`<x><a>a</a><b>b</b><c>c</c></x>` +\n\t\t\t`<x xmlns=\"space1\">` +\n\t\t\t`<c>c1</c>` +\n\t\t\t`<d>d1</d>` +\n\t\t\t`</x>` +\n\t\t\t`<x>` +\n\t\t\t`<e>e1</e>` +\n\t\t\t`</x>` +\n\t\t\t`</top>`,\n\t},\n\t{\n\t\tValue: &struct {\n\t\t\tXMLName Name\n\t\t\tA       string `xml:\"x>a\"`\n\t\t\tB       string `xml:\"x>b\"`\n\t\t\tC       string `xml:\"space x>c\"`\n\t\t\tC1      string `xml:\"space1 x>c\"`\n\t\t\tD1      string `xml:\"space1 x>d\"`\n\t\t}{\n\t\t\tXMLName: Name{\n\t\t\t\tSpace: \"space0\",\n\t\t\t\tLocal: \"top\",\n\t\t\t},\n\t\t\tA:  \"a\",\n\t\t\tB:  \"b\",\n\t\t\tC:  \"c\",\n\t\t\tC1: \"c1\",\n\t\t\tD1: \"d1\",\n\t\t},\n\t\tExpectXML: `<top xmlns=\"space0\">` +\n\t\t\t`<x><a>a</a><b>b</b></x>` +\n\t\t\t`<x xmlns=\"space\"><c>c</c></x>` +\n\t\t\t`<x xmlns=\"space1\">` +\n\t\t\t`<c>c1</c>` +\n\t\t\t`<d>d1</d>` +\n\t\t\t`</x>` +\n\t\t\t`</top>`,\n\t},\n\t{\n\t\tValue: &struct {\n\t\t\tXMLName struct{} `xml:\"top\"`\n\t\t\tB       string   `xml:\"space x>b\"`\n\t\t\tB1      string   `xml:\"space1 x>b\"`\n\t\t}{\n\t\t\tB:  \"b\",\n\t\t\tB1: \"b1\",\n\t\t},\n\t\tExpectXML: `<top>` +\n\t\t\t`<x xmlns=\"space\"><b>b</b></x>` +\n\t\t\t`<x xmlns=\"space1\"><b>b1</b></x>` +\n\t\t\t`</top>`,\n\t},\n\n\t// Test struct embedding\n\t{\n\t\tValue: &EmbedA{\n\t\t\tEmbedC: EmbedC{\n\t\t\t\tFieldA1: \"\", // Shadowed by A.A\n\t\t\t\tFieldA2: \"\", // Shadowed by A.A\n\t\t\t\tFieldB:  \"A.C.B\",\n\t\t\t\tFieldC:  \"A.C.C\",\n\t\t\t},\n\t\t\tEmbedB: EmbedB{\n\t\t\t\tFieldB: \"A.B.B\",\n\t\t\t\tEmbedC: &EmbedC{\n\t\t\t\t\tFieldA1: \"A.B.C.A1\",\n\t\t\t\t\tFieldA2: \"A.B.C.A2\",\n\t\t\t\t\tFieldB:  \"\", // Shadowed by A.B.B\n\t\t\t\t\tFieldC:  \"A.B.C.C\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tFieldA: \"A.A\",\n\t\t},\n\t\tExpectXML: `<EmbedA>` +\n\t\t\t`<FieldB>A.C.B</FieldB>` +\n\t\t\t`<FieldC>A.C.C</FieldC>` +\n\t\t\t`<EmbedB>` +\n\t\t\t`<FieldB>A.B.B</FieldB>` +\n\t\t\t`<FieldA>` +\n\t\t\t`<A1>A.B.C.A1</A1>` +\n\t\t\t`<A2>A.B.C.A2</A2>` +\n\t\t\t`</FieldA>` +\n\t\t\t`<FieldC>A.B.C.C</FieldC>` +\n\t\t\t`</EmbedB>` +\n\t\t\t`<FieldA>A.A</FieldA>` +\n\t\t\t`</EmbedA>`,\n\t},\n\n\t// Test that name casing matters\n\t{\n\t\tValue:     &NameCasing{Xy: \"mixed\", XY: \"upper\", XyA: \"mixedA\", XYA: \"upperA\"},\n\t\tExpectXML: `<casing Xy=\"mixedA\" XY=\"upperA\"><Xy>mixed</Xy><XY>upper</XY></casing>`,\n\t},\n\n\t// Test the order in which the XML element name is chosen\n\t{\n\t\tValue: &NamePrecedence{\n\t\t\tFromTag:     XMLNameWithoutTag{Value: \"A\"},\n\t\t\tFromNameVal: XMLNameWithoutTag{XMLName: Name{Local: \"InXMLName\"}, Value: \"B\"},\n\t\t\tFromNameTag: XMLNameWithTag{Value: \"C\"},\n\t\t\tInFieldName: \"D\",\n\t\t},\n\t\tExpectXML: `<Parent>` +\n\t\t\t`<InTag>A</InTag>` +\n\t\t\t`<InXMLName>B</InXMLName>` +\n\t\t\t`<InXMLNameTag>C</InXMLNameTag>` +\n\t\t\t`<InFieldName>D</InFieldName>` +\n\t\t\t`</Parent>`,\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tValue: &NamePrecedence{\n\t\t\tXMLName:     Name{Local: \"Parent\"},\n\t\t\tFromTag:     XMLNameWithoutTag{XMLName: Name{Local: \"InTag\"}, Value: \"A\"},\n\t\t\tFromNameVal: XMLNameWithoutTag{XMLName: Name{Local: \"FromNameVal\"}, Value: \"B\"},\n\t\t\tFromNameTag: XMLNameWithTag{XMLName: Name{Local: \"InXMLNameTag\"}, Value: \"C\"},\n\t\t\tInFieldName: \"D\",\n\t\t},\n\t\tExpectXML: `<Parent>` +\n\t\t\t`<InTag>A</InTag>` +\n\t\t\t`<FromNameVal>B</FromNameVal>` +\n\t\t\t`<InXMLNameTag>C</InXMLNameTag>` +\n\t\t\t`<InFieldName>D</InFieldName>` +\n\t\t\t`</Parent>`,\n\t\tUnmarshalOnly: true,\n\t},\n\n\t// xml.Name works in a plain field as well.\n\t{\n\t\tValue:     &NameInField{Name{Space: \"ns\", Local: \"foo\"}},\n\t\tExpectXML: `<NameInField><foo xmlns=\"ns\"></foo></NameInField>`,\n\t},\n\t{\n\t\tValue:         &NameInField{Name{Space: \"ns\", Local: \"foo\"}},\n\t\tExpectXML:     `<NameInField><foo xmlns=\"ns\"><ignore></ignore></foo></NameInField>`,\n\t\tUnmarshalOnly: true,\n\t},\n\n\t// Marshaling zero xml.Name uses the tag or field name.\n\t{\n\t\tValue:       &NameInField{},\n\t\tExpectXML:   `<NameInField><foo xmlns=\"ns\"></foo></NameInField>`,\n\t\tMarshalOnly: true,\n\t},\n\n\t// Test attributes\n\t{\n\t\tValue: &AttrTest{\n\t\t\tInt:   8,\n\t\t\tNamed: 9,\n\t\t\tFloat: 23.5,\n\t\t\tUint8: 255,\n\t\t\tBool:  true,\n\t\t\tStr:   \"str\",\n\t\t\tBytes: []byte(\"byt\"),\n\t\t},\n\t\tExpectXML: `<AttrTest Int=\"8\" int=\"9\" Float=\"23.5\" Uint8=\"255\"` +\n\t\t\t` Bool=\"true\" Str=\"str\" Bytes=\"byt\"></AttrTest>`,\n\t},\n\t{\n\t\tValue: &AttrTest{Bytes: []byte{}},\n\t\tExpectXML: `<AttrTest Int=\"0\" int=\"0\" Float=\"0\" Uint8=\"0\"` +\n\t\t\t` Bool=\"false\" Str=\"\" Bytes=\"\"></AttrTest>`,\n\t},\n\t{\n\t\tValue: &OmitAttrTest{\n\t\t\tInt:   8,\n\t\t\tNamed: 9,\n\t\t\tFloat: 23.5,\n\t\t\tUint8: 255,\n\t\t\tBool:  true,\n\t\t\tStr:   \"str\",\n\t\t\tBytes: []byte(\"byt\"),\n\t\t},\n\t\tExpectXML: `<OmitAttrTest Int=\"8\" int=\"9\" Float=\"23.5\" Uint8=\"255\"` +\n\t\t\t` Bool=\"true\" Str=\"str\" Bytes=\"byt\"></OmitAttrTest>`,\n\t},\n\t{\n\t\tValue:     &OmitAttrTest{},\n\t\tExpectXML: `<OmitAttrTest></OmitAttrTest>`,\n\t},\n\n\t// pointer fields\n\t{\n\t\tValue:       &PointerFieldsTest{Name: &nameAttr, Age: &ageAttr, Contents: &contentsAttr},\n\t\tExpectXML:   `<dummy name=\"Sarah\" age=\"12\">lorem ipsum</dummy>`,\n\t\tMarshalOnly: true,\n\t},\n\n\t// empty chardata pointer field\n\t{\n\t\tValue:       &ChardataEmptyTest{},\n\t\tExpectXML:   `<test></test>`,\n\t\tMarshalOnly: true,\n\t},\n\n\t// omitempty on fields\n\t{\n\t\tValue: &OmitFieldTest{\n\t\t\tInt:   8,\n\t\t\tNamed: 9,\n\t\t\tFloat: 23.5,\n\t\t\tUint8: 255,\n\t\t\tBool:  true,\n\t\t\tStr:   \"str\",\n\t\t\tBytes: []byte(\"byt\"),\n\t\t\tPtr:   &PresenceTest{},\n\t\t},\n\t\tExpectXML: `<OmitFieldTest>` +\n\t\t\t`<Int>8</Int>` +\n\t\t\t`<int>9</int>` +\n\t\t\t`<Float>23.5</Float>` +\n\t\t\t`<Uint8>255</Uint8>` +\n\t\t\t`<Bool>true</Bool>` +\n\t\t\t`<Str>str</Str>` +\n\t\t\t`<Bytes>byt</Bytes>` +\n\t\t\t`<Ptr></Ptr>` +\n\t\t\t`</OmitFieldTest>`,\n\t},\n\t{\n\t\tValue:     &OmitFieldTest{},\n\t\tExpectXML: `<OmitFieldTest></OmitFieldTest>`,\n\t},\n\n\t// Test \",any\"\n\t{\n\t\tExpectXML: `<a><nested><value>known</value></nested><other><sub>unknown</sub></other></a>`,\n\t\tValue: &AnyTest{\n\t\t\tNested: \"known\",\n\t\t\tAnyField: AnyHolder{\n\t\t\t\tXMLName: Name{Local: \"other\"},\n\t\t\t\tXML:     \"<sub>unknown</sub>\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tValue: &AnyTest{Nested: \"known\",\n\t\t\tAnyField: AnyHolder{\n\t\t\t\tXML:     \"<unknown/>\",\n\t\t\t\tXMLName: Name{Local: \"AnyField\"},\n\t\t\t},\n\t\t},\n\t\tExpectXML: `<a><nested><value>known</value></nested><AnyField><unknown/></AnyField></a>`,\n\t},\n\t{\n\t\tExpectXML: `<a><nested><value>b</value></nested></a>`,\n\t\tValue: &AnyOmitTest{\n\t\t\tNested: \"b\",\n\t\t},\n\t},\n\t{\n\t\tExpectXML: `<a><nested><value>b</value></nested><c><d>e</d></c><g xmlns=\"f\"><h>i</h></g></a>`,\n\t\tValue: &AnySliceTest{\n\t\t\tNested: \"b\",\n\t\t\tAnyField: []AnyHolder{\n\t\t\t\t{\n\t\t\t\t\tXMLName: Name{Local: \"c\"},\n\t\t\t\t\tXML:     \"<d>e</d>\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tXMLName: Name{Space: \"f\", Local: \"g\"},\n\t\t\t\t\tXML:     \"<h>i</h>\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tExpectXML: `<a><nested><value>b</value></nested></a>`,\n\t\tValue: &AnySliceTest{\n\t\t\tNested: \"b\",\n\t\t},\n\t},\n\n\t// Test recursive types.\n\t{\n\t\tValue: &RecurseA{\n\t\t\tA: \"a1\",\n\t\t\tB: &RecurseB{\n\t\t\t\tA: &RecurseA{\"a2\", nil},\n\t\t\t\tB: \"b1\",\n\t\t\t},\n\t\t},\n\t\tExpectXML: `<RecurseA><A>a1</A><B><A><A>a2</A></A><B>b1</B></B></RecurseA>`,\n\t},\n\n\t// Test ignoring fields via \"-\" tag\n\t{\n\t\tExpectXML: `<IgnoreTest></IgnoreTest>`,\n\t\tValue:     &IgnoreTest{},\n\t},\n\t{\n\t\tExpectXML:   `<IgnoreTest></IgnoreTest>`,\n\t\tValue:       &IgnoreTest{PublicSecret: \"can't tell\"},\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tExpectXML:     `<IgnoreTest><PublicSecret>ignore me</PublicSecret></IgnoreTest>`,\n\t\tValue:         &IgnoreTest{},\n\t\tUnmarshalOnly: true,\n\t},\n\n\t// Test escaping.\n\t{\n\t\tExpectXML: `<a><nested><value>dquote: &#34;; squote: &#39;; ampersand: &amp;; less: &lt;; greater: &gt;;</value></nested><empty></empty></a>`,\n\t\tValue: &AnyTest{\n\t\t\tNested:   `dquote: \"; squote: '; ampersand: &; less: <; greater: >;`,\n\t\t\tAnyField: AnyHolder{XMLName: Name{Local: \"empty\"}},\n\t\t},\n\t},\n\t{\n\t\tExpectXML: `<a><nested><value>newline: &#xA;; cr: &#xD;; tab: &#x9;;</value></nested><AnyField></AnyField></a>`,\n\t\tValue: &AnyTest{\n\t\t\tNested:   \"newline: \\n; cr: \\r; tab: \\t;\",\n\t\t\tAnyField: AnyHolder{XMLName: Name{Local: \"AnyField\"}},\n\t\t},\n\t},\n\t{\n\t\tExpectXML: \"<a><nested><value>1\\r2\\r\\n3\\n\\r4\\n5</value></nested></a>\",\n\t\tValue: &AnyTest{\n\t\t\tNested: \"1\\n2\\n3\\n\\n4\\n5\",\n\t\t},\n\t\tUnmarshalOnly: true,\n\t},\n\t{\n\t\tExpectXML: `<EmbedInt><MyInt>42</MyInt></EmbedInt>`,\n\t\tValue: &EmbedInt{\n\t\t\tMyInt: 42,\n\t\t},\n\t},\n\t// Test omitempty with parent chain; see golang.org/issue/4168.\n\t{\n\t\tExpectXML: `<Strings><A></A></Strings>`,\n\t\tValue:     &Strings{},\n\t},\n\t// Custom marshalers.\n\t{\n\t\tExpectXML: `<MyMarshalerTest>hello world</MyMarshalerTest>`,\n\t\tValue:     &MyMarshalerTest{},\n\t},\n\t{\n\t\tExpectXML: `<MarshalerStruct Foo=\"hello world\"></MarshalerStruct>`,\n\t\tValue:     &MarshalerStruct{},\n\t},\n\t{\n\t\tExpectXML: `<MarshalerValueStruct Foo=\"hello world\"></MarshalerValueStruct>`,\n\t\tValue:     &MarshalerValueStruct{},\n\t},\n\t{\n\t\tExpectXML: `<outer xmlns=\"testns\" int=\"10\"></outer>`,\n\t\tValue:     &OuterStruct{IntAttr: 10},\n\t},\n\t{\n\t\tExpectXML: `<test xmlns=\"outerns\" int=\"10\"></test>`,\n\t\tValue:     &OuterNamedStruct{XMLName: Name{Space: \"outerns\", Local: \"test\"}, IntAttr: 10},\n\t},\n\t{\n\t\tExpectXML: `<test xmlns=\"outerns\" int=\"10\"></test>`,\n\t\tValue:     &OuterNamedOrderedStruct{XMLName: Name{Space: \"outerns\", Local: \"test\"}, IntAttr: 10},\n\t},\n\t{\n\t\tExpectXML: `<outer xmlns=\"testns\" int=\"10\"></outer>`,\n\t\tValue:     &OuterOuterStruct{OuterStruct{IntAttr: 10}},\n\t},\n\t{\n\t\tExpectXML: `<NestedAndChardata><A><B></B><B></B></A>test</NestedAndChardata>`,\n\t\tValue:     &NestedAndChardata{AB: make([]string, 2), Chardata: \"test\"},\n\t},\n\t{\n\t\tExpectXML: `<NestedAndComment><A><B></B><B></B></A><!--test--></NestedAndComment>`,\n\t\tValue:     &NestedAndComment{AB: make([]string, 2), Comment: \"test\"},\n\t},\n\t{\n\t\tExpectXML: `<XMLNSFieldStruct xmlns=\"http://example.com/ns\"><Body>hello world</Body></XMLNSFieldStruct>`,\n\t\tValue:     &XMLNSFieldStruct{Ns: \"http://example.com/ns\", Body: \"hello world\"},\n\t},\n\t{\n\t\tExpectXML: `<testns:test xmlns:testns=\"testns\" xmlns=\"http://example.com/ns\"><Body>hello world</Body></testns:test>`,\n\t\tValue:     &NamedXMLNSFieldStruct{Ns: \"http://example.com/ns\", Body: \"hello world\"},\n\t},\n\t{\n\t\tExpectXML: `<testns:test xmlns:testns=\"testns\"><Body>hello world</Body></testns:test>`,\n\t\tValue:     &NamedXMLNSFieldStruct{Ns: \"\", Body: \"hello world\"},\n\t},\n\t{\n\t\tExpectXML: `<XMLNSFieldStructWithOmitEmpty><Body>hello world</Body></XMLNSFieldStructWithOmitEmpty>`,\n\t\tValue:     &XMLNSFieldStructWithOmitEmpty{Body: \"hello world\"},\n\t},\n\t{\n\t\t// The xmlns attribute must be ignored because the <test>\n\t\t// element is in the empty namespace, so it's not possible\n\t\t// to set the default namespace to something non-empty.\n\t\tExpectXML:   `<test><Body>hello world</Body></test>`,\n\t\tValue:       &NamedXMLNSFieldStructWithEmptyNamespace{Ns: \"foo\", Body: \"hello world\"},\n\t\tMarshalOnly: true,\n\t},\n\t{\n\t\tExpectXML: `<RecursiveXMLNSFieldStruct xmlns=\"foo\"><Body xmlns=\"\"><Text>hello world</Text></Body></RecursiveXMLNSFieldStruct>`,\n\t\tValue: &RecursiveXMLNSFieldStruct{\n\t\t\tNs: \"foo\",\n\t\t\tBody: &RecursiveXMLNSFieldStruct{\n\t\t\t\tText: \"hello world\",\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestMarshal(t *testing.T) {\n\tfor idx, test := range marshalTests {\n\t\tif test.UnmarshalOnly {\n\t\t\tcontinue\n\t\t}\n\t\tdata, err := Marshal(test.Value)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d: marshal(%#v): %s\", idx, test.Value, err)\n\t\t\tcontinue\n\t\t}\n\t\tif got, want := string(data), test.ExpectXML; got != want {\n\t\t\tif strings.Contains(want, \"\\n\") {\n\t\t\t\tt.Errorf(\"#%d: marshal(%#v):\\nHAVE:\\n%s\\nWANT:\\n%s\", idx, test.Value, got, want)\n\t\t\t} else {\n\t\t\t\tt.Errorf(\"#%d: marshal(%#v):\\nhave %#q\\nwant %#q\", idx, test.Value, got, want)\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype AttrParent struct {\n\tX string `xml:\"X>Y,attr\"`\n}\n\ntype BadAttr struct {\n\tName []string `xml:\"name,attr\"`\n}\n\nvar marshalErrorTests = []struct {\n\tValue interface{}\n\tErr   string\n\tKind  reflect.Kind\n}{\n\t{\n\t\tValue: make(chan bool),\n\t\tErr:   \"xml: unsupported type: chan bool\",\n\t\tKind:  reflect.Chan,\n\t},\n\t{\n\t\tValue: map[string]string{\n\t\t\t\"question\": \"What do you get when you multiply six by nine?\",\n\t\t\t\"answer\":   \"42\",\n\t\t},\n\t\tErr:  \"xml: unsupported type: map[string]string\",\n\t\tKind: reflect.Map,\n\t},\n\t{\n\t\tValue: map[*Ship]bool{nil: false},\n\t\tErr:   \"xml: unsupported type: map[*xml.Ship]bool\",\n\t\tKind:  reflect.Map,\n\t},\n\t{\n\t\tValue: &Domain{Comment: []byte(\"f--bar\")},\n\t\tErr:   `xml: comments must not contain \"--\"`,\n\t},\n\t// Reject parent chain with attr, never worked; see golang.org/issue/5033.\n\t{\n\t\tValue: &AttrParent{},\n\t\tErr:   `xml: X>Y chain not valid with attr flag`,\n\t},\n\t{\n\t\tValue: BadAttr{[]string{\"X\", \"Y\"}},\n\t\tErr:   `xml: unsupported type: []string`,\n\t},\n}\n\nvar marshalIndentTests = []struct {\n\tValue     interface{}\n\tPrefix    string\n\tIndent    string\n\tExpectXML string\n}{\n\t{\n\t\tValue: &SecretAgent{\n\t\t\tHandle:    \"007\",\n\t\t\tIdentity:  \"James Bond\",\n\t\t\tObfuscate: \"<redacted/>\",\n\t\t},\n\t\tPrefix:    \"\",\n\t\tIndent:    \"\\t\",\n\t\tExpectXML: fmt.Sprintf(\"<agent handle=\\\"007\\\">\\n\\t<Identity>James Bond</Identity><redacted/>\\n</agent>\"),\n\t},\n}\n\nfunc TestMarshalErrors(t *testing.T) {\n\tfor idx, test := range marshalErrorTests {\n\t\tdata, err := Marshal(test.Value)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"#%d: marshal(%#v) = [success] %q, want error %v\", idx, test.Value, data, test.Err)\n\t\t\tcontinue\n\t\t}\n\t\tif err.Error() != test.Err {\n\t\t\tt.Errorf(\"#%d: marshal(%#v) = [error] %v, want %v\", idx, test.Value, err, test.Err)\n\t\t}\n\t\tif test.Kind != reflect.Invalid {\n\t\t\tif kind := err.(*UnsupportedTypeError).Type.Kind(); kind != test.Kind {\n\t\t\t\tt.Errorf(\"#%d: marshal(%#v) = [error kind] %s, want %s\", idx, test.Value, kind, test.Kind)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Do invertibility testing on the various structures that we test\nfunc TestUnmarshal(t *testing.T) {\n\tfor i, test := range marshalTests {\n\t\tif test.MarshalOnly {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := test.Value.(*Plain); ok {\n\t\t\tcontinue\n\t\t}\n\t\tvt := reflect.TypeOf(test.Value)\n\t\tdest := reflect.New(vt.Elem()).Interface()\n\t\terr := Unmarshal([]byte(test.ExpectXML), dest)\n\n\t\tswitch fix := dest.(type) {\n\t\tcase *Feed:\n\t\t\tfix.Author.InnerXML = \"\"\n\t\t\tfor i := range fix.Entry {\n\t\t\t\tfix.Entry[i].Author.InnerXML = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d: unexpected error: %#v\", i, err)\n\t\t} else if got, want := dest, test.Value; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"#%d: unmarshal(%q):\\nhave %#v\\nwant %#v\", i, test.ExpectXML, got, want)\n\t\t}\n\t}\n}\n\nfunc TestMarshalIndent(t *testing.T) {\n\tfor i, test := range marshalIndentTests {\n\t\tdata, err := MarshalIndent(test.Value, test.Prefix, test.Indent)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d: Error: %s\", i, err)\n\t\t\tcontinue\n\t\t}\n\t\tif got, want := string(data), test.ExpectXML; got != want {\n\t\t\tt.Errorf(\"#%d: MarshalIndent:\\nGot:%s\\nWant:\\n%s\", i, got, want)\n\t\t}\n\t}\n}\n\ntype limitedBytesWriter struct {\n\tw      io.Writer\n\tremain int // until writes fail\n}\n\nfunc (lw *limitedBytesWriter) Write(p []byte) (n int, err error) {\n\tif lw.remain <= 0 {\n\t\tprintln(\"error\")\n\t\treturn 0, errors.New(\"write limit hit\")\n\t}\n\tif len(p) > lw.remain {\n\t\tp = p[:lw.remain]\n\t\tn, _ = lw.w.Write(p)\n\t\tlw.remain = 0\n\t\treturn n, errors.New(\"write limit hit\")\n\t}\n\tn, err = lw.w.Write(p)\n\tlw.remain -= n\n\treturn n, err\n}\n\nfunc TestMarshalWriteErrors(t *testing.T) {\n\tvar buf bytes.Buffer\n\tconst writeCap = 1024\n\tw := &limitedBytesWriter{&buf, writeCap}\n\tenc := NewEncoder(w)\n\tvar err error\n\tvar i int\n\tconst n = 4000\n\tfor i = 1; i <= n; i++ {\n\t\terr = enc.Encode(&Passenger{\n\t\t\tName:   []string{\"Alice\", \"Bob\"},\n\t\t\tWeight: 5,\n\t\t})\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err == nil {\n\t\tt.Error(\"expected an error\")\n\t}\n\tif i == n {\n\t\tt.Errorf(\"expected to fail before the end\")\n\t}\n\tif buf.Len() != writeCap {\n\t\tt.Errorf(\"buf.Len() = %d; want %d\", buf.Len(), writeCap)\n\t}\n}\n\nfunc TestMarshalWriteIOErrors(t *testing.T) {\n\tenc := NewEncoder(errWriter{})\n\n\texpectErr := \"unwritable\"\n\terr := enc.Encode(&Passenger{})\n\tif err == nil || err.Error() != expectErr {\n\t\tt.Errorf(\"EscapeTest = [error] %v, want %v\", err, expectErr)\n\t}\n}\n\nfunc TestMarshalFlush(t *testing.T) {\n\tvar buf bytes.Buffer\n\tenc := NewEncoder(&buf)\n\tif err := enc.EncodeToken(CharData(\"hello world\")); err != nil {\n\t\tt.Fatalf(\"enc.EncodeToken: %v\", err)\n\t}\n\tif buf.Len() > 0 {\n\t\tt.Fatalf(\"enc.EncodeToken caused actual write: %q\", buf.Bytes())\n\t}\n\tif err := enc.Flush(); err != nil {\n\t\tt.Fatalf(\"enc.Flush: %v\", err)\n\t}\n\tif buf.String() != \"hello world\" {\n\t\tt.Fatalf(\"after enc.Flush, buf.String() = %q, want %q\", buf.String(), \"hello world\")\n\t}\n}\n\nvar encodeElementTests = []struct {\n\tdesc      string\n\tvalue     interface{}\n\tstart     StartElement\n\texpectXML string\n}{{\n\tdesc:  \"simple string\",\n\tvalue: \"hello\",\n\tstart: StartElement{\n\t\tName: Name{Local: \"a\"},\n\t},\n\texpectXML: `<a>hello</a>`,\n}, {\n\tdesc:  \"string with added attributes\",\n\tvalue: \"hello\",\n\tstart: StartElement{\n\t\tName: Name{Local: \"a\"},\n\t\tAttr: []Attr{{\n\t\t\tName:  Name{Local: \"x\"},\n\t\t\tValue: \"y\",\n\t\t}, {\n\t\t\tName:  Name{Local: \"foo\"},\n\t\t\tValue: \"bar\",\n\t\t}},\n\t},\n\texpectXML: `<a x=\"y\" foo=\"bar\">hello</a>`,\n}, {\n\tdesc: \"start element with default name space\",\n\tvalue: struct {\n\t\tFoo XMLNameWithNSTag\n\t}{\n\t\tFoo: XMLNameWithNSTag{\n\t\t\tValue: \"hello\",\n\t\t},\n\t},\n\tstart: StartElement{\n\t\tName: Name{Space: \"ns\", Local: \"a\"},\n\t\tAttr: []Attr{{\n\t\t\tName: Name{Local: \"xmlns\"},\n\t\t\t// \"ns\" is the name space defined in XMLNameWithNSTag\n\t\t\tValue: \"ns\",\n\t\t}},\n\t},\n\texpectXML: `<a xmlns=\"ns\"><InXMLNameWithNSTag>hello</InXMLNameWithNSTag></a>`,\n}, {\n\tdesc: \"start element in name space with different default name space\",\n\tvalue: struct {\n\t\tFoo XMLNameWithNSTag\n\t}{\n\t\tFoo: XMLNameWithNSTag{\n\t\t\tValue: \"hello\",\n\t\t},\n\t},\n\tstart: StartElement{\n\t\tName: Name{Space: \"ns2\", Local: \"a\"},\n\t\tAttr: []Attr{{\n\t\t\tName: Name{Local: \"xmlns\"},\n\t\t\t// \"ns\" is the name space defined in XMLNameWithNSTag\n\t\t\tValue: \"ns\",\n\t\t}},\n\t},\n\texpectXML: `<ns2:a xmlns:ns2=\"ns2\" xmlns=\"ns\"><InXMLNameWithNSTag>hello</InXMLNameWithNSTag></ns2:a>`,\n}, {\n\tdesc:  \"XMLMarshaler with start element with default name space\",\n\tvalue: &MyMarshalerTest{},\n\tstart: StartElement{\n\t\tName: Name{Space: \"ns2\", Local: \"a\"},\n\t\tAttr: []Attr{{\n\t\t\tName: Name{Local: \"xmlns\"},\n\t\t\t// \"ns\" is the name space defined in XMLNameWithNSTag\n\t\t\tValue: \"ns\",\n\t\t}},\n\t},\n\texpectXML: `<ns2:a xmlns:ns2=\"ns2\" xmlns=\"ns\">hello world</ns2:a>`,\n}}\n\nfunc TestEncodeElement(t *testing.T) {\n\tfor idx, test := range encodeElementTests {\n\t\tvar buf bytes.Buffer\n\t\tenc := NewEncoder(&buf)\n\t\terr := enc.EncodeElement(test.value, test.start)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"enc.EncodeElement: %v\", err)\n\t\t}\n\t\terr = enc.Flush()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"enc.Flush: %v\", err)\n\t\t}\n\t\tif got, want := buf.String(), test.expectXML; got != want {\n\t\t\tt.Errorf(\"#%d(%s): EncodeElement(%#v, %#v):\\nhave %#q\\nwant %#q\", idx, test.desc, test.value, test.start, got, want)\n\t\t}\n\t}\n}\n\nfunc BenchmarkMarshal(b *testing.B) {\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tMarshal(atomValue)\n\t}\n}\n\nfunc BenchmarkUnmarshal(b *testing.B) {\n\tb.ReportAllocs()\n\txml := []byte(atomXml)\n\tfor i := 0; i < b.N; i++ {\n\t\tUnmarshal(xml, &Feed{})\n\t}\n}\n\n// golang.org/issue/6556\nfunc TestStructPointerMarshal(t *testing.T) {\n\ttype A struct {\n\t\tXMLName string `xml:\"a\"`\n\t\tB       []interface{}\n\t}\n\ttype C struct {\n\t\tXMLName Name\n\t\tValue   string `xml:\"value\"`\n\t}\n\n\ta := new(A)\n\ta.B = append(a.B, &C{\n\t\tXMLName: Name{Local: \"c\"},\n\t\tValue:   \"x\",\n\t})\n\n\tb, err := Marshal(a)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif x := string(b); x != \"<a><c><value>x</value></c></a>\" {\n\t\tt.Fatal(x)\n\t}\n\tvar v A\n\terr = Unmarshal(b, &v)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nvar encodeTokenTests = []struct {\n\tdesc string\n\ttoks []Token\n\twant string\n\terr  string\n}{{\n\tdesc: \"start element with name space\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"local\"}, nil},\n\t},\n\twant: `<space:local xmlns:space=\"space\">`,\n}, {\n\tdesc: \"start element with no name\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"\"}, nil},\n\t},\n\terr: \"xml: start tag with no name\",\n}, {\n\tdesc: \"end element with no name\",\n\ttoks: []Token{\n\t\tEndElement{Name{\"space\", \"\"}},\n\t},\n\terr: \"xml: end tag with no name\",\n}, {\n\tdesc: \"char data\",\n\ttoks: []Token{\n\t\tCharData(\"foo\"),\n\t},\n\twant: `foo`,\n}, {\n\tdesc: \"char data with escaped chars\",\n\ttoks: []Token{\n\t\tCharData(\" \\t\\n\"),\n\t},\n\twant: \" &#x9;\\n\",\n}, {\n\tdesc: \"comment\",\n\ttoks: []Token{\n\t\tComment(\"foo\"),\n\t},\n\twant: `<!--foo-->`,\n}, {\n\tdesc: \"comment with invalid content\",\n\ttoks: []Token{\n\t\tComment(\"foo-->\"),\n\t},\n\terr: \"xml: EncodeToken of Comment containing --> marker\",\n}, {\n\tdesc: \"proc instruction\",\n\ttoks: []Token{\n\t\tProcInst{\"Target\", []byte(\"Instruction\")},\n\t},\n\twant: `<?Target Instruction?>`,\n}, {\n\tdesc: \"proc instruction with empty target\",\n\ttoks: []Token{\n\t\tProcInst{\"\", []byte(\"Instruction\")},\n\t},\n\terr: \"xml: EncodeToken of ProcInst with invalid Target\",\n}, {\n\tdesc: \"proc instruction with bad content\",\n\ttoks: []Token{\n\t\tProcInst{\"\", []byte(\"Instruction?>\")},\n\t},\n\terr: \"xml: EncodeToken of ProcInst with invalid Target\",\n}, {\n\tdesc: \"directive\",\n\ttoks: []Token{\n\t\tDirective(\"foo\"),\n\t},\n\twant: `<!foo>`,\n}, {\n\tdesc: \"more complex directive\",\n\ttoks: []Token{\n\t\tDirective(\"DOCTYPE doc [ <!ELEMENT doc '>'> <!-- com>ment --> ]\"),\n\t},\n\twant: `<!DOCTYPE doc [ <!ELEMENT doc '>'> <!-- com>ment --> ]>`,\n}, {\n\tdesc: \"directive instruction with bad name\",\n\ttoks: []Token{\n\t\tDirective(\"foo>\"),\n\t},\n\terr: \"xml: EncodeToken of Directive containing wrong < or > markers\",\n}, {\n\tdesc: \"end tag without start tag\",\n\ttoks: []Token{\n\t\tEndElement{Name{\"foo\", \"bar\"}},\n\t},\n\terr: \"xml: end tag </bar> without start tag\",\n}, {\n\tdesc: \"mismatching end tag local name\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, nil},\n\t\tEndElement{Name{\"\", \"bar\"}},\n\t},\n\terr:  \"xml: end tag </bar> does not match start tag <foo>\",\n\twant: `<foo>`,\n}, {\n\tdesc: \"mismatching end tag namespace\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, nil},\n\t\tEndElement{Name{\"another\", \"foo\"}},\n\t},\n\terr:  \"xml: end tag </foo> in namespace another does not match start tag <foo> in namespace space\",\n\twant: `<space:foo xmlns:space=\"space\">`,\n}, {\n\tdesc: \"start element with explicit namespace\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"local\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space\"},\n\t\t\t{Name{\"space\", \"foo\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<x:local xmlns:x=\"space\" x:foo=\"value\">`,\n}, {\n\tdesc: \"start element with explicit namespace and colliding prefix\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"local\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space\"},\n\t\t\t{Name{\"space\", \"foo\"}, \"value\"},\n\t\t\t{Name{\"x\", \"bar\"}, \"other\"},\n\t\t}},\n\t},\n\twant: `<x:local xmlns:x_1=\"x\" xmlns:x=\"space\" x:foo=\"value\" x_1:bar=\"other\">`,\n}, {\n\tdesc: \"start element using previously defined namespace\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"local\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"space\", \"x\"}, \"y\"},\n\t\t}},\n\t},\n\twant: `<local xmlns:x=\"space\"><x:foo x:x=\"y\">`,\n}, {\n\tdesc: \"nested name space with same prefix\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space1\"},\n\t\t}},\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space2\"},\n\t\t}},\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"space1\", \"a\"}, \"space1 value\"},\n\t\t\t{Name{\"space2\", \"b\"}, \"space2 value\"},\n\t\t}},\n\t\tEndElement{Name{\"\", \"foo\"}},\n\t\tEndElement{Name{\"\", \"foo\"}},\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"space1\", \"a\"}, \"space1 value\"},\n\t\t\t{Name{\"space2\", \"b\"}, \"space2 value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns:x=\"space1\"><foo xmlns:x=\"space2\"><foo xmlns:space1=\"space1\" space1:a=\"space1 value\" x:b=\"space2 value\"></foo></foo><foo xmlns:space2=\"space2\" x:a=\"space1 value\" space2:b=\"space2 value\">`,\n}, {\n\tdesc: \"start element defining several prefixes for the same name space\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"a\"}, \"space\"},\n\t\t\t{Name{\"xmlns\", \"b\"}, \"space\"},\n\t\t\t{Name{\"space\", \"x\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<a:foo xmlns:a=\"space\" a:x=\"value\">`,\n}, {\n\tdesc: \"nested element redefines name space\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"y\"}, \"space\"},\n\t\t\t{Name{\"space\", \"a\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns:x=\"space\"><x:foo x:a=\"value\">`,\n}, {\n\tdesc: \"nested element creates alias for default name space\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"y\"}, \"space\"},\n\t\t\t{Name{\"space\", \"a\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns=\"space\"><foo xmlns:y=\"space\" y:a=\"value\">`,\n}, {\n\tdesc: \"nested element defines default name space with existing prefix\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"x\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t\t{Name{\"space\", \"a\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns:x=\"space\"><foo xmlns=\"space\" x:a=\"value\">`,\n}, {\n\tdesc: \"nested element uses empty attribute name space when default ns defined\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"attr\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns=\"space\"><foo attr=\"value\">`,\n}, {\n\tdesc: \"redefine xmlns\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"foo\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t},\n\terr: `xml: cannot redefine xmlns attribute prefix`,\n}, {\n\tdesc: \"xmlns with explicit name space #1\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xml\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns=\"space\">`,\n}, {\n\tdesc: \"xmlns with explicit name space #2\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{xmlURL, \"xmlns\"}, \"space\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns=\"space\">`,\n}, {\n\tdesc: \"empty name space declaration is ignored\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"xmlns\", \"foo\"}, \"\"},\n\t\t}},\n\t},\n\twant: `<foo>`,\n}, {\n\tdesc: \"attribute with no name is ignored\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo>`,\n}, {\n\tdesc: \"namespace URL with non-valid name\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"/34\", \"foo\"}, []Attr{\n\t\t\t{Name{\"/34\", \"x\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<_:foo xmlns:_=\"/34\" _:x=\"value\">`,\n}, {\n\tdesc: \"nested element resets default namespace to empty\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"\"},\n\t\t\t{Name{\"\", \"x\"}, \"value\"},\n\t\t\t{Name{\"space\", \"x\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo xmlns=\"space\"><foo xmlns:space=\"space\" xmlns=\"\" x=\"value\" space:x=\"value\">`,\n}, {\n\tdesc: \"nested element requires empty default name space\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"\", \"foo\"}, nil},\n\t},\n\twant: `<foo xmlns=\"space\"><foo xmlns=\"\">`,\n}, {\n\tdesc: \"attribute uses name space from xmlns\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"some/space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"attr\"}, \"value\"},\n\t\t\t{Name{\"some/space\", \"other\"}, \"other value\"},\n\t\t}},\n\t},\n\twant: `<space:foo xmlns:space=\"some/space\" attr=\"value\" space:other=\"other value\">`,\n}, {\n\tdesc: \"default name space should not be used by attributes\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t\t{Name{\"xmlns\", \"bar\"}, \"space\"},\n\t\t\t{Name{\"space\", \"baz\"}, \"foo\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"baz\"}, nil},\n\t\tEndElement{Name{\"space\", \"baz\"}},\n\t\tEndElement{Name{\"space\", \"foo\"}},\n\t},\n\twant: `<foo xmlns:bar=\"space\" xmlns=\"space\" bar:baz=\"foo\"><baz></baz></foo>`,\n}, {\n\tdesc: \"default name space not used by attributes, not explicitly defined\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"space\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t\t{Name{\"space\", \"baz\"}, \"foo\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"baz\"}, nil},\n\t\tEndElement{Name{\"space\", \"baz\"}},\n\t\tEndElement{Name{\"space\", \"foo\"}},\n\t},\n\twant: `<foo xmlns:space=\"space\" xmlns=\"space\" space:baz=\"foo\"><baz></baz></foo>`,\n}, {\n\tdesc: \"impossible xmlns declaration\",\n\ttoks: []Token{\n\t\tStartElement{Name{\"\", \"foo\"}, []Attr{\n\t\t\t{Name{\"\", \"xmlns\"}, \"space\"},\n\t\t}},\n\t\tStartElement{Name{\"space\", \"bar\"}, []Attr{\n\t\t\t{Name{\"space\", \"attr\"}, \"value\"},\n\t\t}},\n\t},\n\twant: `<foo><space:bar xmlns:space=\"space\" space:attr=\"value\">`,\n}}\n\nfunc TestEncodeToken(t *testing.T) {\nloop:\n\tfor i, tt := range encodeTokenTests {\n\t\tvar buf bytes.Buffer\n\t\tenc := NewEncoder(&buf)\n\t\tvar err error\n\t\tfor j, tok := range tt.toks {\n\t\t\terr = enc.EncodeToken(tok)\n\t\t\tif err != nil && j < len(tt.toks)-1 {\n\t\t\t\tt.Errorf(\"#%d %s token #%d: %v\", i, tt.desc, j, err)\n\t\t\t\tcontinue loop\n\t\t\t}\n\t\t}\n\t\terrorf := func(f string, a ...interface{}) {\n\t\t\tt.Errorf(\"#%d %s token #%d:%s\", i, tt.desc, len(tt.toks)-1, fmt.Sprintf(f, a...))\n\t\t}\n\t\tswitch {\n\t\tcase tt.err != \"\" && err == nil:\n\t\t\terrorf(\" expected error; got none\")\n\t\t\tcontinue\n\t\tcase tt.err == \"\" && err != nil:\n\t\t\terrorf(\" got error: %v\", err)\n\t\t\tcontinue\n\t\tcase tt.err != \"\" && err != nil && tt.err != err.Error():\n\t\t\terrorf(\" error mismatch; got %v, want %v\", err, tt.err)\n\t\t\tcontinue\n\t\t}\n\t\tif err := enc.Flush(); err != nil {\n\t\t\terrorf(\" %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif got := buf.String(); got != tt.want {\n\t\t\terrorf(\"\\ngot  %v\\nwant %v\", got, tt.want)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc TestProcInstEncodeToken(t *testing.T) {\n\tvar buf bytes.Buffer\n\tenc := NewEncoder(&buf)\n\n\tif err := enc.EncodeToken(ProcInst{\"xml\", []byte(\"Instruction\")}); err != nil {\n\t\tt.Fatalf(\"enc.EncodeToken: expected to be able to encode xml target ProcInst as first token, %s\", err)\n\t}\n\n\tif err := enc.EncodeToken(ProcInst{\"Target\", []byte(\"Instruction\")}); err != nil {\n\t\tt.Fatalf(\"enc.EncodeToken: expected to be able to add non-xml target ProcInst\")\n\t}\n\n\tif err := enc.EncodeToken(ProcInst{\"xml\", []byte(\"Instruction\")}); err == nil {\n\t\tt.Fatalf(\"enc.EncodeToken: expected to not be allowed to encode xml target ProcInst when not first token\")\n\t}\n}\n\nfunc TestDecodeEncode(t *testing.T) {\n\tvar in, out bytes.Buffer\n\tin.WriteString(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<?Target Instruction?>\n<root>\n</root>\t\n`)\n\tdec := NewDecoder(&in)\n\tenc := NewEncoder(&out)\n\tfor tok, err := dec.Token(); err == nil; tok, err = dec.Token() {\n\t\terr = enc.EncodeToken(tok)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"enc.EncodeToken: Unable to encode token (%#v), %v\", tok, err)\n\t\t}\n\t}\n}\n\n// Issue 9796. Used to fail with GORACE=\"halt_on_error=1\" -race.\nfunc TestRace9796(t *testing.T) {\n\ttype A struct{}\n\ttype B struct {\n\t\tC []A `xml:\"X>Y\"`\n\t}\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tMarshal(B{[]A{{}}})\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc TestIsValidDirective(t *testing.T) {\n\ttestOK := []string{\n\t\t\"<>\",\n\t\t\"< < > >\",\n\t\t\"<!DOCTYPE '<' '>' '>' <!--nothing-->>\",\n\t\t\"<!DOCTYPE doc [ <!ELEMENT doc ANY> <!ELEMENT doc ANY> ]>\",\n\t\t\"<!DOCTYPE doc [ <!ELEMENT doc \\\"ANY> '<' <!E\\\" LEMENT '>' doc ANY> ]>\",\n\t\t\"<!DOCTYPE doc <!-- just>>>> a < comment --> [ <!ITEM anything> ] >\",\n\t}\n\ttestKO := []string{\n\t\t\"<\",\n\t\t\">\",\n\t\t\"<!--\",\n\t\t\"-->\",\n\t\t\"< > > < < >\",\n\t\t\"<!dummy <!-- > -->\",\n\t\t\"<!DOCTYPE doc '>\",\n\t\t\"<!DOCTYPE doc '>'\",\n\t\t\"<!DOCTYPE doc <!--comment>\",\n\t}\n\tfor _, s := range testOK {\n\t\tif !isValidDirective(Directive(s)) {\n\t\t\tt.Errorf(\"Directive %q is expected to be valid\", s)\n\t\t}\n\t}\n\tfor _, s := range testKO {\n\t\tif isValidDirective(Directive(s)) {\n\t\t\tt.Errorf(\"Directive %q is expected to be invalid\", s)\n\t\t}\n\t}\n}\n\n// Issue 11719. EncodeToken used to silently eat tokens with an invalid type.\nfunc TestSimpleUseOfEncodeToken(t *testing.T) {\n\tvar buf bytes.Buffer\n\tenc := NewEncoder(&buf)\n\tif err := enc.EncodeToken(&StartElement{Name: Name{\"\", \"object1\"}}); err == nil {\n\t\tt.Errorf(\"enc.EncodeToken: pointer type should be rejected\")\n\t}\n\tif err := enc.EncodeToken(&EndElement{Name: Name{\"\", \"object1\"}}); err == nil {\n\t\tt.Errorf(\"enc.EncodeToken: pointer type should be rejected\")\n\t}\n\tif err := enc.EncodeToken(StartElement{Name: Name{\"\", \"object2\"}}); err != nil {\n\t\tt.Errorf(\"enc.EncodeToken: StartElement %s\", err)\n\t}\n\tif err := enc.EncodeToken(EndElement{Name: Name{\"\", \"object2\"}}); err != nil {\n\t\tt.Errorf(\"enc.EncodeToken: EndElement %s\", err)\n\t}\n\tif err := enc.EncodeToken(Universe{}); err == nil {\n\t\tt.Errorf(\"enc.EncodeToken: invalid type not caught\")\n\t}\n\tif err := enc.Flush(); err != nil {\n\t\tt.Errorf(\"enc.Flush: %s\", err)\n\t}\n\tif buf.Len() == 0 {\n\t\tt.Errorf(\"enc.EncodeToken: empty buffer\")\n\t}\n\twant := \"<object2></object2>\"\n\tif buf.String() != want {\n\t\tt.Errorf(\"enc.EncodeToken: expected %q; got %q\", want, buf.String())\n\t}\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/read.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"bytes\"\n\t\"encoding\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// BUG(rsc): Mapping between XML elements and data structures is inherently flawed:\n// an XML element is an order-dependent collection of anonymous\n// values, while a data structure is an order-independent collection\n// of named values.\n// See package json for a textual representation more suitable\n// to data structures.\n\n// Unmarshal parses the XML-encoded data and stores the result in\n// the value pointed to by v, which must be an arbitrary struct,\n// slice, or string. Well-formed data that does not fit into v is\n// discarded.\n//\n// Because Unmarshal uses the reflect package, it can only assign\n// to exported (upper case) fields. Unmarshal uses a case-sensitive\n// comparison to match XML element names to tag values and struct\n// field names.\n//\n// Unmarshal maps an XML element to a struct using the following rules.\n// In the rules, the tag of a field refers to the value associated with the\n// key 'xml' in the struct field's tag (see the example above).\n//\n//   - If the struct has a field of type []byte or string with tag\n//     \",innerxml\", Unmarshal accumulates the raw XML nested inside the\n//     element in that field. The rest of the rules still apply.\n//\n//   - If the struct has a field named XMLName of type xml.Name,\n//     Unmarshal records the element name in that field.\n//\n//   - If the XMLName field has an associated tag of the form\n//     \"name\" or \"namespace-URL name\", the XML element must have\n//     the given name (and, optionally, name space) or else Unmarshal\n//     returns an error.\n//\n//   - If the XML element has an attribute whose name matches a\n//     struct field name with an associated tag containing \",attr\" or\n//     the explicit name in a struct field tag of the form \"name,attr\",\n//     Unmarshal records the attribute value in that field.\n//\n//   - If the XML element contains character data, that data is\n//     accumulated in the first struct field that has tag \",chardata\".\n//     The struct field may have type []byte or string.\n//     If there is no such field, the character data is discarded.\n//\n//   - If the XML element contains comments, they are accumulated in\n//     the first struct field that has tag \",comment\".  The struct\n//     field may have type []byte or string. If there is no such\n//     field, the comments are discarded.\n//\n//   - If the XML element contains a sub-element whose name matches\n//     the prefix of a tag formatted as \"a\" or \"a>b>c\", unmarshal\n//     will descend into the XML structure looking for elements with the\n//     given names, and will map the innermost elements to that struct\n//     field. A tag starting with \">\" is equivalent to one starting\n//     with the field name followed by \">\".\n//\n//   - If the XML element contains a sub-element whose name matches\n//     a struct field's XMLName tag and the struct field has no\n//     explicit name tag as per the previous rule, unmarshal maps\n//     the sub-element to that struct field.\n//\n//   - If the XML element contains a sub-element whose name matches a\n//     field without any mode flags (\",attr\", \",chardata\", etc), Unmarshal\n//     maps the sub-element to that struct field.\n//\n//   - If the XML element contains a sub-element that hasn't matched any\n//     of the above rules and the struct has a field with tag \",any\",\n//     unmarshal maps the sub-element to that struct field.\n//\n//   - An anonymous struct field is handled as if the fields of its\n//     value were part of the outer struct.\n//\n//   - A struct field with tag \"-\" is never unmarshalled into.\n//\n// Unmarshal maps an XML element to a string or []byte by saving the\n// concatenation of that element's character data in the string or\n// []byte. The saved []byte is never nil.\n//\n// Unmarshal maps an attribute value to a string or []byte by saving\n// the value in the string or slice.\n//\n// Unmarshal maps an XML element to a slice by extending the length of\n// the slice and mapping the element to the newly created value.\n//\n// Unmarshal maps an XML element or attribute value to a bool by\n// setting it to the boolean value represented by the string.\n//\n// Unmarshal maps an XML element or attribute value to an integer or\n// floating-point field by setting the field to the result of\n// interpreting the string value in decimal. There is no check for\n// overflow.\n//\n// Unmarshal maps an XML element to an xml.Name by recording the\n// element name.\n//\n// Unmarshal maps an XML element to a pointer by setting the pointer\n// to a freshly allocated value and then mapping the element to that value.\nfunc Unmarshal(data []byte, v interface{}) error {\n\treturn NewDecoder(bytes.NewReader(data)).Decode(v)\n}\n\n// Decode works like xml.Unmarshal, except it reads the decoder\n// stream to find the start element.\nfunc (d *Decoder) Decode(v interface{}) error {\n\treturn d.DecodeElement(v, nil)\n}\n\n// DecodeElement works like xml.Unmarshal except that it takes\n// a pointer to the start XML element to decode into v.\n// It is useful when a client reads some raw XML tokens itself\n// but also wants to defer to Unmarshal for some elements.\nfunc (d *Decoder) DecodeElement(v interface{}, start *StartElement) error {\n\tval := reflect.ValueOf(v)\n\tif val.Kind() != reflect.Ptr {\n\t\treturn errors.New(\"non-pointer passed to Unmarshal\")\n\t}\n\treturn d.unmarshal(val.Elem(), start)\n}\n\n// An UnmarshalError represents an error in the unmarshalling process.\ntype UnmarshalError string\n\nfunc (e UnmarshalError) Error() string { return string(e) }\n\n// Unmarshaler is the interface implemented by objects that can unmarshal\n// an XML element description of themselves.\n//\n// UnmarshalXML decodes a single XML element\n// beginning with the given start element.\n// If it returns an error, the outer call to Unmarshal stops and\n// returns that error.\n// UnmarshalXML must consume exactly one XML element.\n// One common implementation strategy is to unmarshal into\n// a separate value with a layout matching the expected XML\n// using d.DecodeElement,  and then to copy the data from\n// that value into the receiver.\n// Another common strategy is to use d.Token to process the\n// XML object one token at a time.\n// UnmarshalXML may not use d.RawToken.\ntype Unmarshaler interface {\n\tUnmarshalXML(d *Decoder, start StartElement) error\n}\n\n// UnmarshalerAttr is the interface implemented by objects that can unmarshal\n// an XML attribute description of themselves.\n//\n// UnmarshalXMLAttr decodes a single XML attribute.\n// If it returns an error, the outer call to Unmarshal stops and\n// returns that error.\n// UnmarshalXMLAttr is used only for struct fields with the\n// \"attr\" option in the field tag.\ntype UnmarshalerAttr interface {\n\tUnmarshalXMLAttr(attr Attr) error\n}\n\n// receiverType returns the receiver type to use in an expression like \"%s.MethodName\".\nfunc receiverType(val interface{}) string {\n\tt := reflect.TypeOf(val)\n\tif t.Name() != \"\" {\n\t\treturn t.String()\n\t}\n\treturn \"(\" + t.String() + \")\"\n}\n\n// unmarshalInterface unmarshals a single XML element into val.\n// start is the opening tag of the element.\nfunc (p *Decoder) unmarshalInterface(val Unmarshaler, start *StartElement) error {\n\t// Record that decoder must stop at end tag corresponding to start.\n\tp.pushEOF()\n\n\tp.unmarshalDepth++\n\terr := val.UnmarshalXML(p, *start)\n\tp.unmarshalDepth--\n\tif err != nil {\n\t\tp.popEOF()\n\t\treturn err\n\t}\n\n\tif !p.popEOF() {\n\t\treturn fmt.Errorf(\"xml: %s.UnmarshalXML did not consume entire <%s> element\", receiverType(val), start.Name.Local)\n\t}\n\n\treturn nil\n}\n\n// unmarshalTextInterface unmarshals a single XML element into val.\n// The chardata contained in the element (but not its children)\n// is passed to the text unmarshaler.\nfunc (p *Decoder) unmarshalTextInterface(val encoding.TextUnmarshaler, start *StartElement) error {\n\tvar buf []byte\n\tdepth := 1\n\tfor depth > 0 {\n\t\tt, err := p.Token()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch t := t.(type) {\n\t\tcase CharData:\n\t\t\tif depth == 1 {\n\t\t\t\tbuf = append(buf, t...)\n\t\t\t}\n\t\tcase StartElement:\n\t\t\tdepth++\n\t\tcase EndElement:\n\t\t\tdepth--\n\t\t}\n\t}\n\treturn val.UnmarshalText(buf)\n}\n\n// unmarshalAttr unmarshals a single XML attribute into val.\nfunc (p *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error {\n\tif val.Kind() == reflect.Ptr {\n\t\tif val.IsNil() {\n\t\t\tval.Set(reflect.New(val.Type().Elem()))\n\t\t}\n\t\tval = val.Elem()\n\t}\n\n\tif val.CanInterface() && val.Type().Implements(unmarshalerAttrType) {\n\t\t// This is an unmarshaler with a non-pointer receiver,\n\t\t// so it's likely to be incorrect, but we do what we're told.\n\t\treturn val.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)\n\t}\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(unmarshalerAttrType) {\n\t\t\treturn pv.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)\n\t\t}\n\t}\n\n\t// Not an UnmarshalerAttr; try encoding.TextUnmarshaler.\n\tif val.CanInterface() && val.Type().Implements(textUnmarshalerType) {\n\t\t// This is an unmarshaler with a non-pointer receiver,\n\t\t// so it's likely to be incorrect, but we do what we're told.\n\t\treturn val.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))\n\t}\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {\n\t\t\treturn pv.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))\n\t\t}\n\t}\n\n\tcopyValue(val, []byte(attr.Value))\n\treturn nil\n}\n\nvar (\n\tunmarshalerType     = reflect.TypeOf((*Unmarshaler)(nil)).Elem()\n\tunmarshalerAttrType = reflect.TypeOf((*UnmarshalerAttr)(nil)).Elem()\n\ttextUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()\n)\n\n// Unmarshal a single XML element into val.\nfunc (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {\n\t// Find start element if we need it.\n\tif start == nil {\n\t\tfor {\n\t\t\ttok, err := p.Token()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif t, ok := tok.(StartElement); ok {\n\t\t\t\tstart = &t\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Load value from interface, but only if the result will be\n\t// usefully addressable.\n\tif val.Kind() == reflect.Interface && !val.IsNil() {\n\t\te := val.Elem()\n\t\tif e.Kind() == reflect.Ptr && !e.IsNil() {\n\t\t\tval = e\n\t\t}\n\t}\n\n\tif val.Kind() == reflect.Ptr {\n\t\tif val.IsNil() {\n\t\t\tval.Set(reflect.New(val.Type().Elem()))\n\t\t}\n\t\tval = val.Elem()\n\t}\n\n\tif val.CanInterface() && val.Type().Implements(unmarshalerType) {\n\t\t// This is an unmarshaler with a non-pointer receiver,\n\t\t// so it's likely to be incorrect, but we do what we're told.\n\t\treturn p.unmarshalInterface(val.Interface().(Unmarshaler), start)\n\t}\n\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(unmarshalerType) {\n\t\t\treturn p.unmarshalInterface(pv.Interface().(Unmarshaler), start)\n\t\t}\n\t}\n\n\tif val.CanInterface() && val.Type().Implements(textUnmarshalerType) {\n\t\treturn p.unmarshalTextInterface(val.Interface().(encoding.TextUnmarshaler), start)\n\t}\n\n\tif val.CanAddr() {\n\t\tpv := val.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {\n\t\t\treturn p.unmarshalTextInterface(pv.Interface().(encoding.TextUnmarshaler), start)\n\t\t}\n\t}\n\n\tvar (\n\t\tdata         []byte\n\t\tsaveData     reflect.Value\n\t\tcomment      []byte\n\t\tsaveComment  reflect.Value\n\t\tsaveXML      reflect.Value\n\t\tsaveXMLIndex int\n\t\tsaveXMLData  []byte\n\t\tsaveAny      reflect.Value\n\t\tsv           reflect.Value\n\t\ttinfo        *typeInfo\n\t\terr          error\n\t)\n\n\tswitch v := val; v.Kind() {\n\tdefault:\n\t\treturn errors.New(\"unknown type \" + v.Type().String())\n\n\tcase reflect.Interface:\n\t\t// TODO: For now, simply ignore the field. In the near\n\t\t//       future we may choose to unmarshal the start\n\t\t//       element on it, if not nil.\n\t\treturn p.Skip()\n\n\tcase reflect.Slice:\n\t\ttyp := v.Type()\n\t\tif typ.Elem().Kind() == reflect.Uint8 {\n\t\t\t// []byte\n\t\t\tsaveData = v\n\t\t\tbreak\n\t\t}\n\n\t\t// Slice of element values.\n\t\t// Grow slice.\n\t\tn := v.Len()\n\t\tif n >= v.Cap() {\n\t\t\tncap := 2 * n\n\t\t\tif ncap < 4 {\n\t\t\t\tncap = 4\n\t\t\t}\n\t\t\tnew := reflect.MakeSlice(typ, n, ncap)\n\t\t\treflect.Copy(new, v)\n\t\t\tv.Set(new)\n\t\t}\n\t\tv.SetLen(n + 1)\n\n\t\t// Recur to read element into slice.\n\t\tif err := p.unmarshal(v.Index(n), start); err != nil {\n\t\t\tv.SetLen(n)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\n\tcase reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.String:\n\t\tsaveData = v\n\n\tcase reflect.Struct:\n\t\ttyp := v.Type()\n\t\tif typ == nameType {\n\t\t\tv.Set(reflect.ValueOf(start.Name))\n\t\t\tbreak\n\t\t}\n\n\t\tsv = v\n\t\ttinfo, err = getTypeInfo(typ)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Validate and assign element name.\n\t\tif tinfo.xmlname != nil {\n\t\t\tfinfo := tinfo.xmlname\n\t\t\tif finfo.name != \"\" && finfo.name != start.Name.Local {\n\t\t\t\treturn UnmarshalError(\"expected element type <\" + finfo.name + \"> but have <\" + start.Name.Local + \">\")\n\t\t\t}\n\t\t\tif finfo.xmlns != \"\" && finfo.xmlns != start.Name.Space {\n\t\t\t\te := \"expected element <\" + finfo.name + \"> in name space \" + finfo.xmlns + \" but have \"\n\t\t\t\tif start.Name.Space == \"\" {\n\t\t\t\t\te += \"no name space\"\n\t\t\t\t} else {\n\t\t\t\t\te += start.Name.Space\n\t\t\t\t}\n\t\t\t\treturn UnmarshalError(e)\n\t\t\t}\n\t\t\tfv := finfo.value(sv)\n\t\t\tif _, ok := fv.Interface().(Name); ok {\n\t\t\t\tfv.Set(reflect.ValueOf(start.Name))\n\t\t\t}\n\t\t}\n\n\t\t// Assign attributes.\n\t\t// Also, determine whether we need to save character data or comments.\n\t\tfor i := range tinfo.fields {\n\t\t\tfinfo := &tinfo.fields[i]\n\t\t\tswitch finfo.flags & fMode {\n\t\t\tcase fAttr:\n\t\t\t\tstrv := finfo.value(sv)\n\t\t\t\t// Look for attribute.\n\t\t\t\tfor _, a := range start.Attr {\n\t\t\t\t\tif a.Name.Local == finfo.name && (finfo.xmlns == \"\" || finfo.xmlns == a.Name.Space) {\n\t\t\t\t\t\tif err := p.unmarshalAttr(strv, a); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase fCharData:\n\t\t\t\tif !saveData.IsValid() {\n\t\t\t\t\tsaveData = finfo.value(sv)\n\t\t\t\t}\n\n\t\t\tcase fComment:\n\t\t\t\tif !saveComment.IsValid() {\n\t\t\t\t\tsaveComment = finfo.value(sv)\n\t\t\t\t}\n\n\t\t\tcase fAny, fAny | fElement:\n\t\t\t\tif !saveAny.IsValid() {\n\t\t\t\t\tsaveAny = finfo.value(sv)\n\t\t\t\t}\n\n\t\t\tcase fInnerXml:\n\t\t\t\tif !saveXML.IsValid() {\n\t\t\t\t\tsaveXML = finfo.value(sv)\n\t\t\t\t\tif p.saved == nil {\n\t\t\t\t\t\tsaveXMLIndex = 0\n\t\t\t\t\t\tp.saved = new(bytes.Buffer)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsaveXMLIndex = p.savedOffset()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find end element.\n\t// Process sub-elements along the way.\nLoop:\n\tfor {\n\t\tvar savedOffset int\n\t\tif saveXML.IsValid() {\n\t\t\tsavedOffset = p.savedOffset()\n\t\t}\n\t\ttok, err := p.Token()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch t := tok.(type) {\n\t\tcase StartElement:\n\t\t\tconsumed := false\n\t\t\tif sv.IsValid() {\n\t\t\t\tconsumed, err = p.unmarshalPath(tinfo, sv, nil, &t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif !consumed && saveAny.IsValid() {\n\t\t\t\t\tconsumed = true\n\t\t\t\t\tif err := p.unmarshal(saveAny, &t); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !consumed {\n\t\t\t\tif err := p.Skip(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase EndElement:\n\t\t\tif saveXML.IsValid() {\n\t\t\t\tsaveXMLData = p.saved.Bytes()[saveXMLIndex:savedOffset]\n\t\t\t\tif saveXMLIndex == 0 {\n\t\t\t\t\tp.saved = nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak Loop\n\n\t\tcase CharData:\n\t\t\tif saveData.IsValid() {\n\t\t\t\tdata = append(data, t...)\n\t\t\t}\n\n\t\tcase Comment:\n\t\t\tif saveComment.IsValid() {\n\t\t\t\tcomment = append(comment, t...)\n\t\t\t}\n\t\t}\n\t}\n\n\tif saveData.IsValid() && saveData.CanInterface() && saveData.Type().Implements(textUnmarshalerType) {\n\t\tif err := saveData.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsaveData = reflect.Value{}\n\t}\n\n\tif saveData.IsValid() && saveData.CanAddr() {\n\t\tpv := saveData.Addr()\n\t\tif pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {\n\t\t\tif err := pv.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsaveData = reflect.Value{}\n\t\t}\n\t}\n\n\tif err := copyValue(saveData, data); err != nil {\n\t\treturn err\n\t}\n\n\tswitch t := saveComment; t.Kind() {\n\tcase reflect.String:\n\t\tt.SetString(string(comment))\n\tcase reflect.Slice:\n\t\tt.Set(reflect.ValueOf(comment))\n\t}\n\n\tswitch t := saveXML; t.Kind() {\n\tcase reflect.String:\n\t\tt.SetString(string(saveXMLData))\n\tcase reflect.Slice:\n\t\tt.Set(reflect.ValueOf(saveXMLData))\n\t}\n\n\treturn nil\n}\n\nfunc copyValue(dst reflect.Value, src []byte) (err error) {\n\tdst0 := dst\n\n\tif dst.Kind() == reflect.Ptr {\n\t\tif dst.IsNil() {\n\t\t\tdst.Set(reflect.New(dst.Type().Elem()))\n\t\t}\n\t\tdst = dst.Elem()\n\t}\n\n\t// Save accumulated data.\n\tswitch dst.Kind() {\n\tcase reflect.Invalid:\n\t\t// Probably a comment.\n\tdefault:\n\t\treturn errors.New(\"cannot unmarshal into \" + dst0.Type().String())\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\titmp, err := strconv.ParseInt(string(src), 10, dst.Type().Bits())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdst.SetInt(itmp)\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\tutmp, err := strconv.ParseUint(string(src), 10, dst.Type().Bits())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdst.SetUint(utmp)\n\tcase reflect.Float32, reflect.Float64:\n\t\tftmp, err := strconv.ParseFloat(string(src), dst.Type().Bits())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdst.SetFloat(ftmp)\n\tcase reflect.Bool:\n\t\tvalue, err := strconv.ParseBool(strings.TrimSpace(string(src)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdst.SetBool(value)\n\tcase reflect.String:\n\t\tdst.SetString(string(src))\n\tcase reflect.Slice:\n\t\tif len(src) == 0 {\n\t\t\t// non-nil to flag presence\n\t\t\tsrc = []byte{}\n\t\t}\n\t\tdst.SetBytes(src)\n\t}\n\treturn nil\n}\n\n// unmarshalPath walks down an XML structure looking for wanted\n// paths, and calls unmarshal on them.\n// The consumed result tells whether XML elements have been consumed\n// from the Decoder until start's matching end element, or if it's\n// still untouched because start is uninteresting for sv's fields.\nfunc (p *Decoder) unmarshalPath(tinfo *typeInfo, sv reflect.Value, parents []string, start *StartElement) (consumed bool, err error) {\n\trecurse := false\nLoop:\n\tfor i := range tinfo.fields {\n\t\tfinfo := &tinfo.fields[i]\n\t\tif finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != \"\" && finfo.xmlns != start.Name.Space {\n\t\t\tcontinue\n\t\t}\n\t\tfor j := range parents {\n\t\t\tif parents[j] != finfo.parents[j] {\n\t\t\t\tcontinue Loop\n\t\t\t}\n\t\t}\n\t\tif len(finfo.parents) == len(parents) && finfo.name == start.Name.Local {\n\t\t\t// It's a perfect match, unmarshal the field.\n\t\t\treturn true, p.unmarshal(finfo.value(sv), start)\n\t\t}\n\t\tif len(finfo.parents) > len(parents) && finfo.parents[len(parents)] == start.Name.Local {\n\t\t\t// It's a prefix for the field. Break and recurse\n\t\t\t// since it's not ok for one field path to be itself\n\t\t\t// the prefix for another field path.\n\t\t\trecurse = true\n\n\t\t\t// We can reuse the same slice as long as we\n\t\t\t// don't try to append to it.\n\t\t\tparents = finfo.parents[:len(parents)+1]\n\t\t\tbreak\n\t\t}\n\t}\n\tif !recurse {\n\t\t// We have no business with this element.\n\t\treturn false, nil\n\t}\n\t// The element is not a perfect match for any field, but one\n\t// or more fields have the path to this element as a parent\n\t// prefix. Recurse and attempt to match these.\n\tfor {\n\t\tvar tok Token\n\t\ttok, err = p.Token()\n\t\tif err != nil {\n\t\t\treturn true, err\n\t\t}\n\t\tswitch t := tok.(type) {\n\t\tcase StartElement:\n\t\t\tconsumed2, err := p.unmarshalPath(tinfo, sv, parents, &t)\n\t\t\tif err != nil {\n\t\t\t\treturn true, err\n\t\t\t}\n\t\t\tif !consumed2 {\n\t\t\t\tif err := p.Skip(); err != nil {\n\t\t\t\t\treturn true, err\n\t\t\t\t}\n\t\t\t}\n\t\tcase EndElement:\n\t\t\treturn true, nil\n\t\t}\n\t}\n}\n\n// Skip reads tokens until it has consumed the end element\n// matching the most recent start element already consumed.\n// It recurs if it encounters a start element, so it can be used to\n// skip nested structures.\n// It returns nil if it finds an end element matching the start\n// element; otherwise it returns an error describing the problem.\nfunc (d *Decoder) Skip() error {\n\tfor {\n\t\ttok, err := d.Token()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch tok.(type) {\n\t\tcase StartElement:\n\t\t\tif err := d.Skip(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase EndElement:\n\t\t\treturn nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/read_test.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\n// Stripped down Atom feed data structures.\n\nfunc TestUnmarshalFeed(t *testing.T) {\n\tvar f Feed\n\tif err := Unmarshal([]byte(atomFeedString), &f); err != nil {\n\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t}\n\tif !reflect.DeepEqual(f, atomFeed) {\n\t\tt.Fatalf(\"have %#v\\nwant %#v\", f, atomFeed)\n\t}\n}\n\n// hget http://codereview.appspot.com/rss/mine/rsc\nconst atomFeedString = `\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"en-us\" updated=\"2009-10-04T01:35:58+00:00\"><title>Code Review - My issues</title><link href=\"http://codereview.appspot.com/\" rel=\"alternate\"></link><link href=\"http://codereview.appspot.com/rss/mine/rsc\" rel=\"self\"></link><id>http://codereview.appspot.com/</id><author><name>rietveld&lt;&gt;</name></author><entry><title>rietveld: an attempt at pubsubhubbub\n</title><link href=\"http://codereview.appspot.com/126085\" rel=\"alternate\"></link><updated>2009-10-04T01:35:58+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:134d9179c41f806be79b3a5f7877d19a</id><summary type=\"html\">\n  An attempt at adding pubsubhubbub support to Rietveld.\nhttp://code.google.com/p/pubsubhubbub\nhttp://code.google.com/p/rietveld/issues/detail?id=155\n\nThe server side of the protocol is trivial:\n  1. add a &amp;lt;link rel=&amp;quot;hub&amp;quot; href=&amp;quot;hub-server&amp;quot;&amp;gt; tag to all\n     feeds that will be pubsubhubbubbed.\n  2. every time one of those feeds changes, tell the hub\n     with a simple POST request.\n\nI have tested this by adding debug prints to a local hub\nserver and checking that the server got the right publish\nrequests.\n\nI can&amp;#39;t quite get the server to work, but I think the bug\nis not in my code.  I think that the server expects to be\nable to grab the feed and see the feed&amp;#39;s actual URL in\nthe link rel=&amp;quot;self&amp;quot;, but the default value for that drops\nthe :port from the URL, and I cannot for the life of me\nfigure out how to get the Atom generator deep inside\ndjango not to do that, or even where it is doing that,\nor even what code is running to generate the Atom feed.\n(I thought I knew but I added some assert False statements\nand it kept running!)\n\nIgnoring that particular problem, I would appreciate\nfeedback on the right way to get the two values at\nthe top of feeds.py marked NOTE(rsc).\n\n\n</summary></entry><entry><title>rietveld: correct tab handling\n</title><link href=\"http://codereview.appspot.com/124106\" rel=\"alternate\"></link><updated>2009-10-03T23:02:17+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:0a2a4f19bb815101f0ba2904aed7c35a</id><summary type=\"html\">\n  This fixes the buggy tab rendering that can be seen at\nhttp://codereview.appspot.com/116075/diff/1/2\n\nThe fundamental problem was that the tab code was\nnot being told what column the text began in, so it\ndidn&amp;#39;t know where to put the tab stops.  Another problem\nwas that some of the code assumed that string byte\noffsets were the same as column offsets, which is only\ntrue if there are no tabs.\n\nIn the process of fixing this, I cleaned up the arguments\nto Fold and ExpandTabs and renamed them Break and\n_ExpandTabs so that I could be sure that I found all the\ncall sites.  I also wanted to verify that ExpandTabs was\nnot being used from outside intra_region_diff.py.\n\n\n</summary></entry></feed> \t   `\n\ntype Feed struct {\n\tXMLName Name      `xml:\"http://www.w3.org/2005/Atom feed\"`\n\tTitle   string    `xml:\"title\"`\n\tId      string    `xml:\"id\"`\n\tLink    []Link    `xml:\"link\"`\n\tUpdated time.Time `xml:\"updated,attr\"`\n\tAuthor  Person    `xml:\"author\"`\n\tEntry   []Entry   `xml:\"entry\"`\n}\n\ntype Entry struct {\n\tTitle   string    `xml:\"title\"`\n\tId      string    `xml:\"id\"`\n\tLink    []Link    `xml:\"link\"`\n\tUpdated time.Time `xml:\"updated\"`\n\tAuthor  Person    `xml:\"author\"`\n\tSummary Text      `xml:\"summary\"`\n}\n\ntype Link struct {\n\tRel  string `xml:\"rel,attr,omitempty\"`\n\tHref string `xml:\"href,attr\"`\n}\n\ntype Person struct {\n\tName     string `xml:\"name\"`\n\tURI      string `xml:\"uri\"`\n\tEmail    string `xml:\"email\"`\n\tInnerXML string `xml:\",innerxml\"`\n}\n\ntype Text struct {\n\tType string `xml:\"type,attr,omitempty\"`\n\tBody string `xml:\",chardata\"`\n}\n\nvar atomFeed = Feed{\n\tXMLName: Name{\"http://www.w3.org/2005/Atom\", \"feed\"},\n\tTitle:   \"Code Review - My issues\",\n\tLink: []Link{\n\t\t{Rel: \"alternate\", Href: \"http://codereview.appspot.com/\"},\n\t\t{Rel: \"self\", Href: \"http://codereview.appspot.com/rss/mine/rsc\"},\n\t},\n\tId:      \"http://codereview.appspot.com/\",\n\tUpdated: ParseTime(\"2009-10-04T01:35:58+00:00\"),\n\tAuthor: Person{\n\t\tName:     \"rietveld<>\",\n\t\tInnerXML: \"<name>rietveld&lt;&gt;</name>\",\n\t},\n\tEntry: []Entry{\n\t\t{\n\t\t\tTitle: \"rietveld: an attempt at pubsubhubbub\\n\",\n\t\t\tLink: []Link{\n\t\t\t\t{Rel: \"alternate\", Href: \"http://codereview.appspot.com/126085\"},\n\t\t\t},\n\t\t\tUpdated: ParseTime(\"2009-10-04T01:35:58+00:00\"),\n\t\t\tAuthor: Person{\n\t\t\t\tName:     \"email-address-removed\",\n\t\t\t\tInnerXML: \"<name>email-address-removed</name>\",\n\t\t\t},\n\t\t\tId: \"urn:md5:134d9179c41f806be79b3a5f7877d19a\",\n\t\t\tSummary: Text{\n\t\t\t\tType: \"html\",\n\t\t\t\tBody: `\n  An attempt at adding pubsubhubbub support to Rietveld.\nhttp://code.google.com/p/pubsubhubbub\nhttp://code.google.com/p/rietveld/issues/detail?id=155\n\nThe server side of the protocol is trivial:\n  1. add a &lt;link rel=&quot;hub&quot; href=&quot;hub-server&quot;&gt; tag to all\n     feeds that will be pubsubhubbubbed.\n  2. every time one of those feeds changes, tell the hub\n     with a simple POST request.\n\nI have tested this by adding debug prints to a local hub\nserver and checking that the server got the right publish\nrequests.\n\nI can&#39;t quite get the server to work, but I think the bug\nis not in my code.  I think that the server expects to be\nable to grab the feed and see the feed&#39;s actual URL in\nthe link rel=&quot;self&quot;, but the default value for that drops\nthe :port from the URL, and I cannot for the life of me\nfigure out how to get the Atom generator deep inside\ndjango not to do that, or even where it is doing that,\nor even what code is running to generate the Atom feed.\n(I thought I knew but I added some assert False statements\nand it kept running!)\n\nIgnoring that particular problem, I would appreciate\nfeedback on the right way to get the two values at\nthe top of feeds.py marked NOTE(rsc).\n\n\n`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTitle: \"rietveld: correct tab handling\\n\",\n\t\t\tLink: []Link{\n\t\t\t\t{Rel: \"alternate\", Href: \"http://codereview.appspot.com/124106\"},\n\t\t\t},\n\t\t\tUpdated: ParseTime(\"2009-10-03T23:02:17+00:00\"),\n\t\t\tAuthor: Person{\n\t\t\t\tName:     \"email-address-removed\",\n\t\t\t\tInnerXML: \"<name>email-address-removed</name>\",\n\t\t\t},\n\t\t\tId: \"urn:md5:0a2a4f19bb815101f0ba2904aed7c35a\",\n\t\t\tSummary: Text{\n\t\t\t\tType: \"html\",\n\t\t\t\tBody: `\n  This fixes the buggy tab rendering that can be seen at\nhttp://codereview.appspot.com/116075/diff/1/2\n\nThe fundamental problem was that the tab code was\nnot being told what column the text began in, so it\ndidn&#39;t know where to put the tab stops.  Another problem\nwas that some of the code assumed that string byte\noffsets were the same as column offsets, which is only\ntrue if there are no tabs.\n\nIn the process of fixing this, I cleaned up the arguments\nto Fold and ExpandTabs and renamed them Break and\n_ExpandTabs so that I could be sure that I found all the\ncall sites.  I also wanted to verify that ExpandTabs was\nnot being used from outside intra_region_diff.py.\n\n\n`,\n\t\t\t},\n\t\t},\n\t},\n}\n\nconst pathTestString = `\n<Result>\n    <Before>1</Before>\n    <Items>\n        <Item1>\n            <Value>A</Value>\n        </Item1>\n        <Item2>\n            <Value>B</Value>\n        </Item2>\n        <Item1>\n            <Value>C</Value>\n            <Value>D</Value>\n        </Item1>\n        <_>\n            <Value>E</Value>\n        </_>\n    </Items>\n    <After>2</After>\n</Result>\n`\n\ntype PathTestItem struct {\n\tValue string\n}\n\ntype PathTestA struct {\n\tItems         []PathTestItem `xml:\">Item1\"`\n\tBefore, After string\n}\n\ntype PathTestB struct {\n\tOther         []PathTestItem `xml:\"Items>Item1\"`\n\tBefore, After string\n}\n\ntype PathTestC struct {\n\tValues1       []string `xml:\"Items>Item1>Value\"`\n\tValues2       []string `xml:\"Items>Item2>Value\"`\n\tBefore, After string\n}\n\ntype PathTestSet struct {\n\tItem1 []PathTestItem\n}\n\ntype PathTestD struct {\n\tOther         PathTestSet `xml:\"Items\"`\n\tBefore, After string\n}\n\ntype PathTestE struct {\n\tUnderline     string `xml:\"Items>_>Value\"`\n\tBefore, After string\n}\n\nvar pathTests = []interface{}{\n\t&PathTestA{Items: []PathTestItem{{\"A\"}, {\"D\"}}, Before: \"1\", After: \"2\"},\n\t&PathTestB{Other: []PathTestItem{{\"A\"}, {\"D\"}}, Before: \"1\", After: \"2\"},\n\t&PathTestC{Values1: []string{\"A\", \"C\", \"D\"}, Values2: []string{\"B\"}, Before: \"1\", After: \"2\"},\n\t&PathTestD{Other: PathTestSet{Item1: []PathTestItem{{\"A\"}, {\"D\"}}}, Before: \"1\", After: \"2\"},\n\t&PathTestE{Underline: \"E\", Before: \"1\", After: \"2\"},\n}\n\nfunc TestUnmarshalPaths(t *testing.T) {\n\tfor _, pt := range pathTests {\n\t\tv := reflect.New(reflect.TypeOf(pt).Elem()).Interface()\n\t\tif err := Unmarshal([]byte(pathTestString), v); err != nil {\n\t\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t\t}\n\t\tif !reflect.DeepEqual(v, pt) {\n\t\t\tt.Fatalf(\"have %#v\\nwant %#v\", v, pt)\n\t\t}\n\t}\n}\n\ntype BadPathTestA struct {\n\tFirst  string `xml:\"items>item1\"`\n\tOther  string `xml:\"items>item2\"`\n\tSecond string `xml:\"items\"`\n}\n\ntype BadPathTestB struct {\n\tOther  string `xml:\"items>item2>value\"`\n\tFirst  string `xml:\"items>item1\"`\n\tSecond string `xml:\"items>item1>value\"`\n}\n\ntype BadPathTestC struct {\n\tFirst  string\n\tSecond string `xml:\"First\"`\n}\n\ntype BadPathTestD struct {\n\tBadPathEmbeddedA\n\tBadPathEmbeddedB\n}\n\ntype BadPathEmbeddedA struct {\n\tFirst string\n}\n\ntype BadPathEmbeddedB struct {\n\tSecond string `xml:\"First\"`\n}\n\nvar badPathTests = []struct {\n\tv, e interface{}\n}{\n\t{&BadPathTestA{}, &TagPathError{reflect.TypeOf(BadPathTestA{}), \"First\", \"items>item1\", \"Second\", \"items\"}},\n\t{&BadPathTestB{}, &TagPathError{reflect.TypeOf(BadPathTestB{}), \"First\", \"items>item1\", \"Second\", \"items>item1>value\"}},\n\t{&BadPathTestC{}, &TagPathError{reflect.TypeOf(BadPathTestC{}), \"First\", \"\", \"Second\", \"First\"}},\n\t{&BadPathTestD{}, &TagPathError{reflect.TypeOf(BadPathTestD{}), \"First\", \"\", \"Second\", \"First\"}},\n}\n\nfunc TestUnmarshalBadPaths(t *testing.T) {\n\tfor _, tt := range badPathTests {\n\t\terr := Unmarshal([]byte(pathTestString), tt.v)\n\t\tif !reflect.DeepEqual(err, tt.e) {\n\t\t\tt.Fatalf(\"Unmarshal with %#v didn't fail properly:\\nhave %#v,\\nwant %#v\", tt.v, err, tt.e)\n\t\t}\n\t}\n}\n\nconst OK = \"OK\"\nconst withoutNameTypeData = `\n<?xml version=\"1.0\" charset=\"utf-8\"?>\n<Test3 Attr=\"OK\" />`\n\ntype TestThree struct {\n\tXMLName Name   `xml:\"Test3\"`\n\tAttr    string `xml:\",attr\"`\n}\n\nfunc TestUnmarshalWithoutNameType(t *testing.T) {\n\tvar x TestThree\n\tif err := Unmarshal([]byte(withoutNameTypeData), &x); err != nil {\n\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t}\n\tif x.Attr != OK {\n\t\tt.Fatalf(\"have %v\\nwant %v\", x.Attr, OK)\n\t}\n}\n\nfunc TestUnmarshalAttr(t *testing.T) {\n\ttype ParamVal struct {\n\t\tInt int `xml:\"int,attr\"`\n\t}\n\n\ttype ParamPtr struct {\n\t\tInt *int `xml:\"int,attr\"`\n\t}\n\n\ttype ParamStringPtr struct {\n\t\tInt *string `xml:\"int,attr\"`\n\t}\n\n\tx := []byte(`<Param int=\"1\" />`)\n\n\tp1 := &ParamPtr{}\n\tif err := Unmarshal(x, p1); err != nil {\n\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t}\n\tif p1.Int == nil {\n\t\tt.Fatalf(\"Unmarshal failed in to *int field\")\n\t} else if *p1.Int != 1 {\n\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p1.Int, 1)\n\t}\n\n\tp2 := &ParamVal{}\n\tif err := Unmarshal(x, p2); err != nil {\n\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t}\n\tif p2.Int != 1 {\n\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p2.Int, 1)\n\t}\n\n\tp3 := &ParamStringPtr{}\n\tif err := Unmarshal(x, p3); err != nil {\n\t\tt.Fatalf(\"Unmarshal: %s\", err)\n\t}\n\tif p3.Int == nil {\n\t\tt.Fatalf(\"Unmarshal failed in to *string field\")\n\t} else if *p3.Int != \"1\" {\n\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p3.Int, 1)\n\t}\n}\n\ntype Tables struct {\n\tHTable string `xml:\"http://www.w3.org/TR/html4/ table\"`\n\tFTable string `xml:\"http://www.w3schools.com/furniture table\"`\n}\n\nvar tables = []struct {\n\txml string\n\ttab Tables\n\tns  string\n}{\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table xmlns=\"http://www.w3.org/TR/html4/\">hello</table>` +\n\t\t\t`<table xmlns=\"http://www.w3schools.com/furniture\">world</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{\"hello\", \"world\"},\n\t},\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table xmlns=\"http://www.w3schools.com/furniture\">world</table>` +\n\t\t\t`<table xmlns=\"http://www.w3.org/TR/html4/\">hello</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{\"hello\", \"world\"},\n\t},\n\t{\n\t\txml: `<Tables xmlns:f=\"http://www.w3schools.com/furniture\" xmlns:h=\"http://www.w3.org/TR/html4/\">` +\n\t\t\t`<f:table>world</f:table>` +\n\t\t\t`<h:table>hello</h:table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{\"hello\", \"world\"},\n\t},\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table>bogus</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{},\n\t},\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table>only</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{HTable: \"only\"},\n\t\tns:  \"http://www.w3.org/TR/html4/\",\n\t},\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table>only</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{FTable: \"only\"},\n\t\tns:  \"http://www.w3schools.com/furniture\",\n\t},\n\t{\n\t\txml: `<Tables>` +\n\t\t\t`<table>only</table>` +\n\t\t\t`</Tables>`,\n\t\ttab: Tables{},\n\t\tns:  \"something else entirely\",\n\t},\n}\n\nfunc TestUnmarshalNS(t *testing.T) {\n\tfor i, tt := range tables {\n\t\tvar dst Tables\n\t\tvar err error\n\t\tif tt.ns != \"\" {\n\t\t\td := NewDecoder(strings.NewReader(tt.xml))\n\t\t\td.DefaultSpace = tt.ns\n\t\t\terr = d.Decode(&dst)\n\t\t} else {\n\t\t\terr = Unmarshal([]byte(tt.xml), &dst)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d: Unmarshal: %v\", i, err)\n\t\t\tcontinue\n\t\t}\n\t\twant := tt.tab\n\t\tif dst != want {\n\t\t\tt.Errorf(\"#%d: dst=%+v, want %+v\", i, dst, want)\n\t\t}\n\t}\n}\n\nfunc TestRoundTrip(t *testing.T) {\n\t// From issue 7535\n\tconst s = `<ex:element xmlns:ex=\"http://example.com/schema\"></ex:element>`\n\tin := bytes.NewBufferString(s)\n\tfor i := 0; i < 10; i++ {\n\t\tout := &bytes.Buffer{}\n\t\td := NewDecoder(in)\n\t\te := NewEncoder(out)\n\n\t\tfor {\n\t\t\tt, err := d.Token()\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"failed:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\te.EncodeToken(t)\n\t\t}\n\t\te.Flush()\n\t\tin = out\n\t}\n\tif got := in.String(); got != s {\n\t\tt.Errorf(\"have: %q\\nwant: %q\\n\", got, s)\n\t}\n}\n\nfunc TestMarshalNS(t *testing.T) {\n\tdst := Tables{\"hello\", \"world\"}\n\tdata, err := Marshal(&dst)\n\tif err != nil {\n\t\tt.Fatalf(\"Marshal: %v\", err)\n\t}\n\twant := `<Tables><table xmlns=\"http://www.w3.org/TR/html4/\">hello</table><table xmlns=\"http://www.w3schools.com/furniture\">world</table></Tables>`\n\tstr := string(data)\n\tif str != want {\n\t\tt.Errorf(\"have: %q\\nwant: %q\\n\", str, want)\n\t}\n}\n\ntype TableAttrs struct {\n\tTAttr TAttr\n}\n\ntype TAttr struct {\n\tHTable string `xml:\"http://www.w3.org/TR/html4/ table,attr\"`\n\tFTable string `xml:\"http://www.w3schools.com/furniture table,attr\"`\n\tLang   string `xml:\"http://www.w3.org/XML/1998/namespace lang,attr,omitempty\"`\n\tOther1 string `xml:\"http://golang.org/xml/ other,attr,omitempty\"`\n\tOther2 string `xml:\"http://golang.org/xmlfoo/ other,attr,omitempty\"`\n\tOther3 string `xml:\"http://golang.org/json/ other,attr,omitempty\"`\n\tOther4 string `xml:\"http://golang.org/2/json/ other,attr,omitempty\"`\n}\n\nvar tableAttrs = []struct {\n\txml string\n\ttab TableAttrs\n\tns  string\n}{\n\t{\n\t\txml: `<TableAttrs xmlns:f=\"http://www.w3schools.com/furniture\" xmlns:h=\"http://www.w3.org/TR/html4/\"><TAttr ` +\n\t\t\t`h:table=\"hello\" f:table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"hello\", FTable: \"world\"}},\n\t},\n\t{\n\t\txml: `<TableAttrs><TAttr xmlns:f=\"http://www.w3schools.com/furniture\" xmlns:h=\"http://www.w3.org/TR/html4/\" ` +\n\t\t\t`h:table=\"hello\" f:table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"hello\", FTable: \"world\"}},\n\t},\n\t{\n\t\txml: `<TableAttrs><TAttr ` +\n\t\t\t`h:table=\"hello\" f:table=\"world\" xmlns:f=\"http://www.w3schools.com/furniture\" xmlns:h=\"http://www.w3.org/TR/html4/\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"hello\", FTable: \"world\"}},\n\t},\n\t{\n\t\t// Default space does not apply to attribute names.\n\t\txml: `<TableAttrs xmlns=\"http://www.w3schools.com/furniture\" xmlns:h=\"http://www.w3.org/TR/html4/\"><TAttr ` +\n\t\t\t`h:table=\"hello\" table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"hello\", FTable: \"\"}},\n\t},\n\t{\n\t\t// Default space does not apply to attribute names.\n\t\txml: `<TableAttrs xmlns:f=\"http://www.w3schools.com/furniture\"><TAttr xmlns=\"http://www.w3.org/TR/html4/\" ` +\n\t\t\t`table=\"hello\" f:table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"\", FTable: \"world\"}},\n\t},\n\t{\n\t\txml: `<TableAttrs><TAttr ` +\n\t\t\t`table=\"bogus\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{},\n\t},\n\t{\n\t\t// Default space does not apply to attribute names.\n\t\txml: `<TableAttrs xmlns:h=\"http://www.w3.org/TR/html4/\"><TAttr ` +\n\t\t\t`h:table=\"hello\" table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"hello\", FTable: \"\"}},\n\t\tns:  \"http://www.w3schools.com/furniture\",\n\t},\n\t{\n\t\t// Default space does not apply to attribute names.\n\t\txml: `<TableAttrs xmlns:f=\"http://www.w3schools.com/furniture\"><TAttr ` +\n\t\t\t`table=\"hello\" f:table=\"world\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{TAttr{HTable: \"\", FTable: \"world\"}},\n\t\tns:  \"http://www.w3.org/TR/html4/\",\n\t},\n\t{\n\t\txml: `<TableAttrs><TAttr ` +\n\t\t\t`table=\"bogus\" ` +\n\t\t\t`/></TableAttrs>`,\n\t\ttab: TableAttrs{},\n\t\tns:  \"something else entirely\",\n\t},\n}\n\nfunc TestUnmarshalNSAttr(t *testing.T) {\n\tfor i, tt := range tableAttrs {\n\t\tvar dst TableAttrs\n\t\tvar err error\n\t\tif tt.ns != \"\" {\n\t\t\td := NewDecoder(strings.NewReader(tt.xml))\n\t\t\td.DefaultSpace = tt.ns\n\t\t\terr = d.Decode(&dst)\n\t\t} else {\n\t\t\terr = Unmarshal([]byte(tt.xml), &dst)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d: Unmarshal: %v\", i, err)\n\t\t\tcontinue\n\t\t}\n\t\twant := tt.tab\n\t\tif dst != want {\n\t\t\tt.Errorf(\"#%d: dst=%+v, want %+v\", i, dst, want)\n\t\t}\n\t}\n}\n\nfunc TestMarshalNSAttr(t *testing.T) {\n\tsrc := TableAttrs{TAttr{\"hello\", \"world\", \"en_US\", \"other1\", \"other2\", \"other3\", \"other4\"}}\n\tdata, err := Marshal(&src)\n\tif err != nil {\n\t\tt.Fatalf(\"Marshal: %v\", err)\n\t}\n\twant := `<TableAttrs><TAttr xmlns:json_1=\"http://golang.org/2/json/\" xmlns:json=\"http://golang.org/json/\" xmlns:_xmlfoo=\"http://golang.org/xmlfoo/\" xmlns:_xml=\"http://golang.org/xml/\" xmlns:furniture=\"http://www.w3schools.com/furniture\" xmlns:html4=\"http://www.w3.org/TR/html4/\" html4:table=\"hello\" furniture:table=\"world\" xml:lang=\"en_US\" _xml:other=\"other1\" _xmlfoo:other=\"other2\" json:other=\"other3\" json_1:other=\"other4\"></TAttr></TableAttrs>`\n\tstr := string(data)\n\tif str != want {\n\t\tt.Errorf(\"Marshal:\\nhave: %#q\\nwant: %#q\\n\", str, want)\n\t}\n\n\tvar dst TableAttrs\n\tif err := Unmarshal(data, &dst); err != nil {\n\t\tt.Errorf(\"Unmarshal: %v\", err)\n\t}\n\n\tif dst != src {\n\t\tt.Errorf(\"Unmarshal = %q, want %q\", dst, src)\n\t}\n}\n\ntype MyCharData struct {\n\tbody string\n}\n\nfunc (m *MyCharData) UnmarshalXML(d *Decoder, start StartElement) error {\n\tfor {\n\t\tt, err := d.Token()\n\t\tif err == io.EOF { // found end of element\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif char, ok := t.(CharData); ok {\n\t\t\tm.body += string(char)\n\t\t}\n\t}\n\treturn nil\n}\n\nvar _ Unmarshaler = (*MyCharData)(nil)\n\nfunc (m *MyCharData) UnmarshalXMLAttr(attr Attr) error {\n\tpanic(\"must not call\")\n}\n\ntype MyAttr struct {\n\tattr string\n}\n\nfunc (m *MyAttr) UnmarshalXMLAttr(attr Attr) error {\n\tm.attr = attr.Value\n\treturn nil\n}\n\nvar _ UnmarshalerAttr = (*MyAttr)(nil)\n\ntype MyStruct struct {\n\tData *MyCharData\n\tAttr *MyAttr `xml:\",attr\"`\n\n\tData2 MyCharData\n\tAttr2 MyAttr `xml:\",attr\"`\n}\n\nfunc TestUnmarshaler(t *testing.T) {\n\txml := `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\t\t<MyStruct Attr=\"attr1\" Attr2=\"attr2\">\n\t\t<Data>hello <!-- comment -->world</Data>\n\t\t<Data2>howdy <!-- comment -->world</Data2>\n\t\t</MyStruct>\n\t`\n\n\tvar m MyStruct\n\tif err := Unmarshal([]byte(xml), &m); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif m.Data == nil || m.Attr == nil || m.Data.body != \"hello world\" || m.Attr.attr != \"attr1\" || m.Data2.body != \"howdy world\" || m.Attr2.attr != \"attr2\" {\n\t\tt.Errorf(\"m=%#+v\\n\", m)\n\t}\n}\n\ntype Pea struct {\n\tCotelydon string\n}\n\ntype Pod struct {\n\tPea interface{} `xml:\"Pea\"`\n}\n\n// https://golang.org/issue/6836\nfunc TestUnmarshalIntoInterface(t *testing.T) {\n\tpod := new(Pod)\n\tpod.Pea = new(Pea)\n\txml := `<Pod><Pea><Cotelydon>Green stuff</Cotelydon></Pea></Pod>`\n\terr := Unmarshal([]byte(xml), pod)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to unmarshal %q: %v\", xml, err)\n\t}\n\tpea, ok := pod.Pea.(*Pea)\n\tif !ok {\n\t\tt.Fatalf(\"unmarshalled into wrong type: have %T want *Pea\", pod.Pea)\n\t}\n\thave, want := pea.Cotelydon, \"Green stuff\"\n\tif have != want {\n\t\tt.Errorf(\"failed to unmarshal into interface, have %q want %q\", have, want)\n\t}\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/typeinfo.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// typeInfo holds details for the xml representation of a type.\ntype typeInfo struct {\n\txmlname *fieldInfo\n\tfields  []fieldInfo\n}\n\n// fieldInfo holds details for the xml representation of a single field.\ntype fieldInfo struct {\n\tidx     []int\n\tname    string\n\txmlns   string\n\tflags   fieldFlags\n\tparents []string\n}\n\ntype fieldFlags int\n\nconst (\n\tfElement fieldFlags = 1 << iota\n\tfAttr\n\tfCharData\n\tfInnerXml\n\tfComment\n\tfAny\n\n\tfOmitEmpty\n\n\tfMode = fElement | fAttr | fCharData | fInnerXml | fComment | fAny\n)\n\nvar tinfoMap = make(map[reflect.Type]*typeInfo)\nvar tinfoLock sync.RWMutex\n\nvar nameType = reflect.TypeOf(Name{})\n\n// getTypeInfo returns the typeInfo structure with details necessary\n// for marshalling and unmarshalling typ.\nfunc getTypeInfo(typ reflect.Type) (*typeInfo, error) {\n\ttinfoLock.RLock()\n\ttinfo, ok := tinfoMap[typ]\n\ttinfoLock.RUnlock()\n\tif ok {\n\t\treturn tinfo, nil\n\t}\n\ttinfo = &typeInfo{}\n\tif typ.Kind() == reflect.Struct && typ != nameType {\n\t\tn := typ.NumField()\n\t\tfor i := 0; i < n; i++ {\n\t\t\tf := typ.Field(i)\n\t\t\tif f.PkgPath != \"\" || f.Tag.Get(\"xml\") == \"-\" {\n\t\t\t\tcontinue // Private field\n\t\t\t}\n\n\t\t\t// For embedded structs, embed its fields.\n\t\t\tif f.Anonymous {\n\t\t\t\tt := f.Type\n\t\t\t\tif t.Kind() == reflect.Ptr {\n\t\t\t\t\tt = t.Elem()\n\t\t\t\t}\n\t\t\t\tif t.Kind() == reflect.Struct {\n\t\t\t\t\tinner, err := getTypeInfo(t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tif tinfo.xmlname == nil {\n\t\t\t\t\t\ttinfo.xmlname = inner.xmlname\n\t\t\t\t\t}\n\t\t\t\t\tfor _, finfo := range inner.fields {\n\t\t\t\t\t\tfinfo.idx = append([]int{i}, finfo.idx...)\n\t\t\t\t\t\tif err := addFieldInfo(typ, tinfo, &finfo); err != nil {\n\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfinfo, err := structFieldInfo(typ, &f)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif f.Name == \"XMLName\" {\n\t\t\t\ttinfo.xmlname = finfo\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Add the field if it doesn't conflict with other fields.\n\t\t\tif err := addFieldInfo(typ, tinfo, finfo); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\ttinfoLock.Lock()\n\ttinfoMap[typ] = tinfo\n\ttinfoLock.Unlock()\n\treturn tinfo, nil\n}\n\n// structFieldInfo builds and returns a fieldInfo for f.\nfunc structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {\n\tfinfo := &fieldInfo{idx: f.Index}\n\n\t// Split the tag from the xml namespace if necessary.\n\ttag := f.Tag.Get(\"xml\")\n\tif i := strings.Index(tag, \" \"); i >= 0 {\n\t\tfinfo.xmlns, tag = tag[:i], tag[i+1:]\n\t}\n\n\t// Parse flags.\n\ttokens := strings.Split(tag, \",\")\n\tif len(tokens) == 1 {\n\t\tfinfo.flags = fElement\n\t} else {\n\t\ttag = tokens[0]\n\t\tfor _, flag := range tokens[1:] {\n\t\t\tswitch flag {\n\t\t\tcase \"attr\":\n\t\t\t\tfinfo.flags |= fAttr\n\t\t\tcase \"chardata\":\n\t\t\t\tfinfo.flags |= fCharData\n\t\t\tcase \"innerxml\":\n\t\t\t\tfinfo.flags |= fInnerXml\n\t\t\tcase \"comment\":\n\t\t\t\tfinfo.flags |= fComment\n\t\t\tcase \"any\":\n\t\t\t\tfinfo.flags |= fAny\n\t\t\tcase \"omitempty\":\n\t\t\t\tfinfo.flags |= fOmitEmpty\n\t\t\t}\n\t\t}\n\n\t\t// Validate the flags used.\n\t\tvalid := true\n\t\tswitch mode := finfo.flags & fMode; mode {\n\t\tcase 0:\n\t\t\tfinfo.flags |= fElement\n\t\tcase fAttr, fCharData, fInnerXml, fComment, fAny:\n\t\t\tif f.Name == \"XMLName\" || tag != \"\" && mode != fAttr {\n\t\t\t\tvalid = false\n\t\t\t}\n\t\tdefault:\n\t\t\t// This will also catch multiple modes in a single field.\n\t\t\tvalid = false\n\t\t}\n\t\tif finfo.flags&fMode == fAny {\n\t\t\tfinfo.flags |= fElement\n\t\t}\n\t\tif finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {\n\t\t\tvalid = false\n\t\t}\n\t\tif !valid {\n\t\t\treturn nil, fmt.Errorf(\"xml: invalid tag in field %s of type %s: %q\",\n\t\t\t\tf.Name, typ, f.Tag.Get(\"xml\"))\n\t\t}\n\t}\n\n\t// Use of xmlns without a name is not allowed.\n\tif finfo.xmlns != \"\" && tag == \"\" {\n\t\treturn nil, fmt.Errorf(\"xml: namespace without name in field %s of type %s: %q\",\n\t\t\tf.Name, typ, f.Tag.Get(\"xml\"))\n\t}\n\n\tif f.Name == \"XMLName\" {\n\t\t// The XMLName field records the XML element name. Don't\n\t\t// process it as usual because its name should default to\n\t\t// empty rather than to the field name.\n\t\tfinfo.name = tag\n\t\treturn finfo, nil\n\t}\n\n\tif tag == \"\" {\n\t\t// If the name part of the tag is completely empty, get\n\t\t// default from XMLName of underlying struct if feasible,\n\t\t// or field name otherwise.\n\t\tif xmlname := lookupXMLName(f.Type); xmlname != nil {\n\t\t\tfinfo.xmlns, finfo.name = xmlname.xmlns, xmlname.name\n\t\t} else {\n\t\t\tfinfo.name = f.Name\n\t\t}\n\t\treturn finfo, nil\n\t}\n\n\tif finfo.xmlns == \"\" && finfo.flags&fAttr == 0 {\n\t\t// If it's an element no namespace specified, get the default\n\t\t// from the XMLName of enclosing struct if possible.\n\t\tif xmlname := lookupXMLName(typ); xmlname != nil {\n\t\t\tfinfo.xmlns = xmlname.xmlns\n\t\t}\n\t}\n\n\t// Prepare field name and parents.\n\tparents := strings.Split(tag, \">\")\n\tif parents[0] == \"\" {\n\t\tparents[0] = f.Name\n\t}\n\tif parents[len(parents)-1] == \"\" {\n\t\treturn nil, fmt.Errorf(\"xml: trailing '>' in field %s of type %s\", f.Name, typ)\n\t}\n\tfinfo.name = parents[len(parents)-1]\n\tif len(parents) > 1 {\n\t\tif (finfo.flags & fElement) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"xml: %s chain not valid with %s flag\", tag, strings.Join(tokens[1:], \",\"))\n\t\t}\n\t\tfinfo.parents = parents[:len(parents)-1]\n\t}\n\n\t// If the field type has an XMLName field, the names must match\n\t// so that the behavior of both marshalling and unmarshalling\n\t// is straightforward and unambiguous.\n\tif finfo.flags&fElement != 0 {\n\t\tftyp := f.Type\n\t\txmlname := lookupXMLName(ftyp)\n\t\tif xmlname != nil && xmlname.name != finfo.name {\n\t\t\treturn nil, fmt.Errorf(\"xml: name %q in tag of %s.%s conflicts with name %q in %s.XMLName\",\n\t\t\t\tfinfo.name, typ, f.Name, xmlname.name, ftyp)\n\t\t}\n\t}\n\treturn finfo, nil\n}\n\n// lookupXMLName returns the fieldInfo for typ's XMLName field\n// in case it exists and has a valid xml field tag, otherwise\n// it returns nil.\nfunc lookupXMLName(typ reflect.Type) (xmlname *fieldInfo) {\n\tfor typ.Kind() == reflect.Ptr {\n\t\ttyp = typ.Elem()\n\t}\n\tif typ.Kind() != reflect.Struct {\n\t\treturn nil\n\t}\n\tfor i, n := 0, typ.NumField(); i < n; i++ {\n\t\tf := typ.Field(i)\n\t\tif f.Name != \"XMLName\" {\n\t\t\tcontinue\n\t\t}\n\t\tfinfo, err := structFieldInfo(typ, &f)\n\t\tif finfo.name != \"\" && err == nil {\n\t\t\treturn finfo\n\t\t}\n\t\t// Also consider errors as a non-existent field tag\n\t\t// and let getTypeInfo itself report the error.\n\t\tbreak\n\t}\n\treturn nil\n}\n\nfunc min(a, b int) int {\n\tif a <= b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// addFieldInfo adds finfo to tinfo.fields if there are no\n// conflicts, or if conflicts arise from previous fields that were\n// obtained from deeper embedded structures than finfo. In the latter\n// case, the conflicting entries are dropped.\n// A conflict occurs when the path (parent + name) to a field is\n// itself a prefix of another path, or when two paths match exactly.\n// It is okay for field paths to share a common, shorter prefix.\nfunc addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {\n\tvar conflicts []int\nLoop:\n\t// First, figure all conflicts. Most working code will have none.\n\tfor i := range tinfo.fields {\n\t\toldf := &tinfo.fields[i]\n\t\tif oldf.flags&fMode != newf.flags&fMode {\n\t\t\tcontinue\n\t\t}\n\t\tif oldf.xmlns != \"\" && newf.xmlns != \"\" && oldf.xmlns != newf.xmlns {\n\t\t\tcontinue\n\t\t}\n\t\tminl := min(len(newf.parents), len(oldf.parents))\n\t\tfor p := 0; p < minl; p++ {\n\t\t\tif oldf.parents[p] != newf.parents[p] {\n\t\t\t\tcontinue Loop\n\t\t\t}\n\t\t}\n\t\tif len(oldf.parents) > len(newf.parents) {\n\t\t\tif oldf.parents[len(newf.parents)] == newf.name {\n\t\t\t\tconflicts = append(conflicts, i)\n\t\t\t}\n\t\t} else if len(oldf.parents) < len(newf.parents) {\n\t\t\tif newf.parents[len(oldf.parents)] == oldf.name {\n\t\t\t\tconflicts = append(conflicts, i)\n\t\t\t}\n\t\t} else {\n\t\t\tif newf.name == oldf.name {\n\t\t\t\tconflicts = append(conflicts, i)\n\t\t\t}\n\t\t}\n\t}\n\t// Without conflicts, add the new field and return.\n\tif conflicts == nil {\n\t\ttinfo.fields = append(tinfo.fields, *newf)\n\t\treturn nil\n\t}\n\n\t// If any conflict is shallower, ignore the new field.\n\t// This matches the Go field resolution on embedding.\n\tfor _, i := range conflicts {\n\t\tif len(tinfo.fields[i].idx) < len(newf.idx) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Otherwise, if any of them is at the same depth level, it's an error.\n\tfor _, i := range conflicts {\n\t\toldf := &tinfo.fields[i]\n\t\tif len(oldf.idx) == len(newf.idx) {\n\t\t\tf1 := typ.FieldByIndex(oldf.idx)\n\t\t\tf2 := typ.FieldByIndex(newf.idx)\n\t\t\treturn &TagPathError{typ, f1.Name, f1.Tag.Get(\"xml\"), f2.Name, f2.Tag.Get(\"xml\")}\n\t\t}\n\t}\n\n\t// Otherwise, the new field is shallower, and thus takes precedence,\n\t// so drop the conflicting fields from tinfo and append the new one.\n\tfor c := len(conflicts) - 1; c >= 0; c-- {\n\t\ti := conflicts[c]\n\t\tcopy(tinfo.fields[i:], tinfo.fields[i+1:])\n\t\ttinfo.fields = tinfo.fields[:len(tinfo.fields)-1]\n\t}\n\ttinfo.fields = append(tinfo.fields, *newf)\n\treturn nil\n}\n\n// A TagPathError represents an error in the unmarshalling process\n// caused by the use of field tags with conflicting paths.\ntype TagPathError struct {\n\tStruct       reflect.Type\n\tField1, Tag1 string\n\tField2, Tag2 string\n}\n\nfunc (e *TagPathError) Error() string {\n\treturn fmt.Sprintf(\"%s field %q with tag %q conflicts with field %q with tag %q\", e.Struct, e.Field1, e.Tag1, e.Field2, e.Tag2)\n}\n\n// value returns v's field value corresponding to finfo.\n// It's equivalent to v.FieldByIndex(finfo.idx), but initializes\n// and dereferences pointers as necessary.\nfunc (finfo *fieldInfo) value(v reflect.Value) reflect.Value {\n\tfor i, x := range finfo.idx {\n\t\tif i > 0 {\n\t\t\tt := v.Type()\n\t\t\tif t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {\n\t\t\t\tif v.IsNil() {\n\t\t\t\t\tv.Set(reflect.New(v.Type().Elem()))\n\t\t\t\t}\n\t\t\t\tv = v.Elem()\n\t\t\t}\n\t\t}\n\t\tv = v.Field(x)\n\t}\n\treturn v\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/xml.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package xml implements a simple XML 1.0 parser that\n// understands XML name spaces.\npackage xml\n\n// References:\n//    Annotated XML spec: http://www.xml.com/axml/testaxml.htm\n//    XML name spaces: http://www.w3.org/TR/REC-xml-names/\n\n// TODO(rsc):\n//\tTest error handling.\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\n// A SyntaxError represents a syntax error in the XML input stream.\ntype SyntaxError struct {\n\tMsg  string\n\tLine int\n}\n\nfunc (e *SyntaxError) Error() string {\n\treturn \"XML syntax error on line \" + strconv.Itoa(e.Line) + \": \" + e.Msg\n}\n\n// A Name represents an XML name (Local) annotated with a name space\n// identifier (Space). In tokens returned by Decoder.Token, the Space\n// identifier is given as a canonical URL, not the short prefix used in\n// the document being parsed.\n//\n// As a special case, XML namespace declarations will use the literal\n// string \"xmlns\" for the Space field instead of the fully resolved URL.\n// See Encoder.EncodeToken for more information on namespace encoding\n// behaviour.\ntype Name struct {\n\tSpace, Local string\n}\n\n// isNamespace reports whether the name is a namespace-defining name.\nfunc (name Name) isNamespace() bool {\n\treturn name.Local == \"xmlns\" || name.Space == \"xmlns\"\n}\n\n// An Attr represents an attribute in an XML element (Name=Value).\ntype Attr struct {\n\tName  Name\n\tValue string\n}\n\n// A Token is an interface holding one of the token types:\n// StartElement, EndElement, CharData, Comment, ProcInst, or Directive.\ntype Token interface{}\n\n// A StartElement represents an XML start element.\ntype StartElement struct {\n\tName Name\n\tAttr []Attr\n}\n\nfunc (e StartElement) Copy() StartElement {\n\tattrs := make([]Attr, len(e.Attr))\n\tcopy(attrs, e.Attr)\n\te.Attr = attrs\n\treturn e\n}\n\n// End returns the corresponding XML end element.\nfunc (e StartElement) End() EndElement {\n\treturn EndElement{e.Name}\n}\n\n// setDefaultNamespace sets the namespace of the element\n// as the default for all elements contained within it.\nfunc (e *StartElement) setDefaultNamespace() {\n\tif e.Name.Space == \"\" {\n\t\t// If there's no namespace on the element, don't\n\t\t// set the default. Strictly speaking this might be wrong, as\n\t\t// we can't tell if the element had no namespace set\n\t\t// or was just using the default namespace.\n\t\treturn\n\t}\n\t// Don't add a default name space if there's already one set.\n\tfor _, attr := range e.Attr {\n\t\tif attr.Name.Space == \"\" && attr.Name.Local == \"xmlns\" {\n\t\t\treturn\n\t\t}\n\t}\n\te.Attr = append(e.Attr, Attr{\n\t\tName: Name{\n\t\t\tLocal: \"xmlns\",\n\t\t},\n\t\tValue: e.Name.Space,\n\t})\n}\n\n// An EndElement represents an XML end element.\ntype EndElement struct {\n\tName Name\n}\n\n// A CharData represents XML character data (raw text),\n// in which XML escape sequences have been replaced by\n// the characters they represent.\ntype CharData []byte\n\nfunc makeCopy(b []byte) []byte {\n\tb1 := make([]byte, len(b))\n\tcopy(b1, b)\n\treturn b1\n}\n\nfunc (c CharData) Copy() CharData { return CharData(makeCopy(c)) }\n\n// A Comment represents an XML comment of the form <!--comment-->.\n// The bytes do not include the <!-- and --> comment markers.\ntype Comment []byte\n\nfunc (c Comment) Copy() Comment { return Comment(makeCopy(c)) }\n\n// A ProcInst represents an XML processing instruction of the form <?target inst?>\ntype ProcInst struct {\n\tTarget string\n\tInst   []byte\n}\n\nfunc (p ProcInst) Copy() ProcInst {\n\tp.Inst = makeCopy(p.Inst)\n\treturn p\n}\n\n// A Directive represents an XML directive of the form <!text>.\n// The bytes do not include the <! and > markers.\ntype Directive []byte\n\nfunc (d Directive) Copy() Directive { return Directive(makeCopy(d)) }\n\n// CopyToken returns a copy of a Token.\nfunc CopyToken(t Token) Token {\n\tswitch v := t.(type) {\n\tcase CharData:\n\t\treturn v.Copy()\n\tcase Comment:\n\t\treturn v.Copy()\n\tcase Directive:\n\t\treturn v.Copy()\n\tcase ProcInst:\n\t\treturn v.Copy()\n\tcase StartElement:\n\t\treturn v.Copy()\n\t}\n\treturn t\n}\n\n// A Decoder represents an XML parser reading a particular input stream.\n// The parser assumes that its input is encoded in UTF-8.\ntype Decoder struct {\n\t// Strict defaults to true, enforcing the requirements\n\t// of the XML specification.\n\t// If set to false, the parser allows input containing common\n\t// mistakes:\n\t//\t* If an element is missing an end tag, the parser invents\n\t//\t  end tags as necessary to keep the return values from Token\n\t//\t  properly balanced.\n\t//\t* In attribute values and character data, unknown or malformed\n\t//\t  character entities (sequences beginning with &) are left alone.\n\t//\n\t// Setting:\n\t//\n\t//\td.Strict = false;\n\t//\td.AutoClose = HTMLAutoClose;\n\t//\td.Entity = HTMLEntity\n\t//\n\t// creates a parser that can handle typical HTML.\n\t//\n\t// Strict mode does not enforce the requirements of the XML name spaces TR.\n\t// In particular it does not reject name space tags using undefined prefixes.\n\t// Such tags are recorded with the unknown prefix as the name space URL.\n\tStrict bool\n\n\t// When Strict == false, AutoClose indicates a set of elements to\n\t// consider closed immediately after they are opened, regardless\n\t// of whether an end element is present.\n\tAutoClose []string\n\n\t// Entity can be used to map non-standard entity names to string replacements.\n\t// The parser behaves as if these standard mappings are present in the map,\n\t// regardless of the actual map content:\n\t//\n\t//\t\"lt\": \"<\",\n\t//\t\"gt\": \">\",\n\t//\t\"amp\": \"&\",\n\t//\t\"apos\": \"'\",\n\t//\t\"quot\": `\"`,\n\tEntity map[string]string\n\n\t// CharsetReader, if non-nil, defines a function to generate\n\t// charset-conversion readers, converting from the provided\n\t// non-UTF-8 charset into UTF-8. If CharsetReader is nil or\n\t// returns an error, parsing stops with an error. One of the\n\t// the CharsetReader's result values must be non-nil.\n\tCharsetReader func(charset string, input io.Reader) (io.Reader, error)\n\n\t// DefaultSpace sets the default name space used for unadorned tags,\n\t// as if the entire XML stream were wrapped in an element containing\n\t// the attribute xmlns=\"DefaultSpace\".\n\tDefaultSpace string\n\n\tr              io.ByteReader\n\tbuf            bytes.Buffer\n\tsaved          *bytes.Buffer\n\tstk            *stack\n\tfree           *stack\n\tneedClose      bool\n\ttoClose        Name\n\tnextToken      Token\n\tnextByte       int\n\tns             map[string]string\n\terr            error\n\tline           int\n\toffset         int64\n\tunmarshalDepth int\n}\n\n// NewDecoder creates a new XML parser reading from r.\n// If r does not implement io.ByteReader, NewDecoder will\n// do its own buffering.\nfunc NewDecoder(r io.Reader) *Decoder {\n\td := &Decoder{\n\t\tns:       make(map[string]string),\n\t\tnextByte: -1,\n\t\tline:     1,\n\t\tStrict:   true,\n\t}\n\td.switchToReader(r)\n\treturn d\n}\n\n// Token returns the next XML token in the input stream.\n// At the end of the input stream, Token returns nil, io.EOF.\n//\n// Slices of bytes in the returned token data refer to the\n// parser's internal buffer and remain valid only until the next\n// call to Token. To acquire a copy of the bytes, call CopyToken\n// or the token's Copy method.\n//\n// Token expands self-closing elements such as <br/>\n// into separate start and end elements returned by successive calls.\n//\n// Token guarantees that the StartElement and EndElement\n// tokens it returns are properly nested and matched:\n// if Token encounters an unexpected end element,\n// it will return an error.\n//\n// Token implements XML name spaces as described by\n// http://www.w3.org/TR/REC-xml-names/.  Each of the\n// Name structures contained in the Token has the Space\n// set to the URL identifying its name space when known.\n// If Token encounters an unrecognized name space prefix,\n// it uses the prefix as the Space rather than report an error.\nfunc (d *Decoder) Token() (t Token, err error) {\n\tif d.stk != nil && d.stk.kind == stkEOF {\n\t\terr = io.EOF\n\t\treturn\n\t}\n\tif d.nextToken != nil {\n\t\tt = d.nextToken\n\t\td.nextToken = nil\n\t} else if t, err = d.rawToken(); err != nil {\n\t\treturn\n\t}\n\n\tif !d.Strict {\n\t\tif t1, ok := d.autoClose(t); ok {\n\t\t\td.nextToken = t\n\t\t\tt = t1\n\t\t}\n\t}\n\tswitch t1 := t.(type) {\n\tcase StartElement:\n\t\t// In XML name spaces, the translations listed in the\n\t\t// attributes apply to the element name and\n\t\t// to the other attribute names, so process\n\t\t// the translations first.\n\t\tfor _, a := range t1.Attr {\n\t\t\tif a.Name.Space == \"xmlns\" {\n\t\t\t\tv, ok := d.ns[a.Name.Local]\n\t\t\t\td.pushNs(a.Name.Local, v, ok)\n\t\t\t\td.ns[a.Name.Local] = a.Value\n\t\t\t}\n\t\t\tif a.Name.Space == \"\" && a.Name.Local == \"xmlns\" {\n\t\t\t\t// Default space for untagged names\n\t\t\t\tv, ok := d.ns[\"\"]\n\t\t\t\td.pushNs(\"\", v, ok)\n\t\t\t\td.ns[\"\"] = a.Value\n\t\t\t}\n\t\t}\n\n\t\td.translate(&t1.Name, true)\n\t\tfor i := range t1.Attr {\n\t\t\td.translate(&t1.Attr[i].Name, false)\n\t\t}\n\t\td.pushElement(t1.Name)\n\t\tt = t1\n\n\tcase EndElement:\n\t\td.translate(&t1.Name, true)\n\t\tif !d.popElement(&t1) {\n\t\t\treturn nil, d.err\n\t\t}\n\t\tt = t1\n\t}\n\treturn\n}\n\nconst xmlURL = \"http://www.w3.org/XML/1998/namespace\"\n\n// Apply name space translation to name n.\n// The default name space (for Space==\"\")\n// applies only to element names, not to attribute names.\nfunc (d *Decoder) translate(n *Name, isElementName bool) {\n\tswitch {\n\tcase n.Space == \"xmlns\":\n\t\treturn\n\tcase n.Space == \"\" && !isElementName:\n\t\treturn\n\tcase n.Space == \"xml\":\n\t\tn.Space = xmlURL\n\tcase n.Space == \"\" && n.Local == \"xmlns\":\n\t\treturn\n\t}\n\tif v, ok := d.ns[n.Space]; ok {\n\t\tn.Space = v\n\t} else if n.Space == \"\" {\n\t\tn.Space = d.DefaultSpace\n\t}\n}\n\nfunc (d *Decoder) switchToReader(r io.Reader) {\n\t// Get efficient byte at a time reader.\n\t// Assume that if reader has its own\n\t// ReadByte, it's efficient enough.\n\t// Otherwise, use bufio.\n\tif rb, ok := r.(io.ByteReader); ok {\n\t\td.r = rb\n\t} else {\n\t\td.r = bufio.NewReader(r)\n\t}\n}\n\n// Parsing state - stack holds old name space translations\n// and the current set of open elements. The translations to pop when\n// ending a given tag are *below* it on the stack, which is\n// more work but forced on us by XML.\ntype stack struct {\n\tnext *stack\n\tkind int\n\tname Name\n\tok   bool\n}\n\nconst (\n\tstkStart = iota\n\tstkNs\n\tstkEOF\n)\n\nfunc (d *Decoder) push(kind int) *stack {\n\ts := d.free\n\tif s != nil {\n\t\td.free = s.next\n\t} else {\n\t\ts = new(stack)\n\t}\n\ts.next = d.stk\n\ts.kind = kind\n\td.stk = s\n\treturn s\n}\n\nfunc (d *Decoder) pop() *stack {\n\ts := d.stk\n\tif s != nil {\n\t\td.stk = s.next\n\t\ts.next = d.free\n\t\td.free = s\n\t}\n\treturn s\n}\n\n// Record that after the current element is finished\n// (that element is already pushed on the stack)\n// Token should return EOF until popEOF is called.\nfunc (d *Decoder) pushEOF() {\n\t// Walk down stack to find Start.\n\t// It might not be the top, because there might be stkNs\n\t// entries above it.\n\tstart := d.stk\n\tfor start.kind != stkStart {\n\t\tstart = start.next\n\t}\n\t// The stkNs entries below a start are associated with that\n\t// element too; skip over them.\n\tfor start.next != nil && start.next.kind == stkNs {\n\t\tstart = start.next\n\t}\n\ts := d.free\n\tif s != nil {\n\t\td.free = s.next\n\t} else {\n\t\ts = new(stack)\n\t}\n\ts.kind = stkEOF\n\ts.next = start.next\n\tstart.next = s\n}\n\n// Undo a pushEOF.\n// The element must have been finished, so the EOF should be at the top of the stack.\nfunc (d *Decoder) popEOF() bool {\n\tif d.stk == nil || d.stk.kind != stkEOF {\n\t\treturn false\n\t}\n\td.pop()\n\treturn true\n}\n\n// Record that we are starting an element with the given name.\nfunc (d *Decoder) pushElement(name Name) {\n\ts := d.push(stkStart)\n\ts.name = name\n}\n\n// Record that we are changing the value of ns[local].\n// The old value is url, ok.\nfunc (d *Decoder) pushNs(local string, url string, ok bool) {\n\ts := d.push(stkNs)\n\ts.name.Local = local\n\ts.name.Space = url\n\ts.ok = ok\n}\n\n// Creates a SyntaxError with the current line number.\nfunc (d *Decoder) syntaxError(msg string) error {\n\treturn &SyntaxError{Msg: msg, Line: d.line}\n}\n\n// Record that we are ending an element with the given name.\n// The name must match the record at the top of the stack,\n// which must be a pushElement record.\n// After popping the element, apply any undo records from\n// the stack to restore the name translations that existed\n// before we saw this element.\nfunc (d *Decoder) popElement(t *EndElement) bool {\n\ts := d.pop()\n\tname := t.Name\n\tswitch {\n\tcase s == nil || s.kind != stkStart:\n\t\td.err = d.syntaxError(\"unexpected end element </\" + name.Local + \">\")\n\t\treturn false\n\tcase s.name.Local != name.Local:\n\t\tif !d.Strict {\n\t\t\td.needClose = true\n\t\t\td.toClose = t.Name\n\t\t\tt.Name = s.name\n\t\t\treturn true\n\t\t}\n\t\td.err = d.syntaxError(\"element <\" + s.name.Local + \"> closed by </\" + name.Local + \">\")\n\t\treturn false\n\tcase s.name.Space != name.Space:\n\t\td.err = d.syntaxError(\"element <\" + s.name.Local + \"> in space \" + s.name.Space +\n\t\t\t\"closed by </\" + name.Local + \"> in space \" + name.Space)\n\t\treturn false\n\t}\n\n\t// Pop stack until a Start or EOF is on the top, undoing the\n\t// translations that were associated with the element we just closed.\n\tfor d.stk != nil && d.stk.kind != stkStart && d.stk.kind != stkEOF {\n\t\ts := d.pop()\n\t\tif s.ok {\n\t\t\td.ns[s.name.Local] = s.name.Space\n\t\t} else {\n\t\t\tdelete(d.ns, s.name.Local)\n\t\t}\n\t}\n\n\treturn true\n}\n\n// If the top element on the stack is autoclosing and\n// t is not the end tag, invent the end tag.\nfunc (d *Decoder) autoClose(t Token) (Token, bool) {\n\tif d.stk == nil || d.stk.kind != stkStart {\n\t\treturn nil, false\n\t}\n\tname := strings.ToLower(d.stk.name.Local)\n\tfor _, s := range d.AutoClose {\n\t\tif strings.ToLower(s) == name {\n\t\t\t// This one should be auto closed if t doesn't close it.\n\t\t\tet, ok := t.(EndElement)\n\t\t\tif !ok || et.Name.Local != name {\n\t\t\t\treturn EndElement{d.stk.name}, true\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil, false\n}\n\nvar errRawToken = errors.New(\"xml: cannot use RawToken from UnmarshalXML method\")\n\n// RawToken is like Token but does not verify that\n// start and end elements match and does not translate\n// name space prefixes to their corresponding URLs.\nfunc (d *Decoder) RawToken() (Token, error) {\n\tif d.unmarshalDepth > 0 {\n\t\treturn nil, errRawToken\n\t}\n\treturn d.rawToken()\n}\n\nfunc (d *Decoder) rawToken() (Token, error) {\n\tif d.err != nil {\n\t\treturn nil, d.err\n\t}\n\tif d.needClose {\n\t\t// The last element we read was self-closing and\n\t\t// we returned just the StartElement half.\n\t\t// Return the EndElement half now.\n\t\td.needClose = false\n\t\treturn EndElement{d.toClose}, nil\n\t}\n\n\tb, ok := d.getc()\n\tif !ok {\n\t\treturn nil, d.err\n\t}\n\n\tif b != '<' {\n\t\t// Text section.\n\t\td.ungetc(b)\n\t\tdata := d.text(-1, false)\n\t\tif data == nil {\n\t\t\treturn nil, d.err\n\t\t}\n\t\treturn CharData(data), nil\n\t}\n\n\tif b, ok = d.mustgetc(); !ok {\n\t\treturn nil, d.err\n\t}\n\tswitch b {\n\tcase '/':\n\t\t// </: End element\n\t\tvar name Name\n\t\tif name, ok = d.nsname(); !ok {\n\t\t\tif d.err == nil {\n\t\t\t\td.err = d.syntaxError(\"expected element name after </\")\n\t\t\t}\n\t\t\treturn nil, d.err\n\t\t}\n\t\td.space()\n\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\treturn nil, d.err\n\t\t}\n\t\tif b != '>' {\n\t\t\td.err = d.syntaxError(\"invalid characters between </\" + name.Local + \" and >\")\n\t\t\treturn nil, d.err\n\t\t}\n\t\treturn EndElement{name}, nil\n\n\tcase '?':\n\t\t// <?: Processing instruction.\n\t\tvar target string\n\t\tif target, ok = d.name(); !ok {\n\t\t\tif d.err == nil {\n\t\t\t\td.err = d.syntaxError(\"expected target name after <?\")\n\t\t\t}\n\t\t\treturn nil, d.err\n\t\t}\n\t\td.space()\n\t\td.buf.Reset()\n\t\tvar b0 byte\n\t\tfor {\n\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\td.buf.WriteByte(b)\n\t\t\tif b0 == '?' && b == '>' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tb0 = b\n\t\t}\n\t\tdata := d.buf.Bytes()\n\t\tdata = data[0 : len(data)-2] // chop ?>\n\n\t\tif target == \"xml\" {\n\t\t\tcontent := string(data)\n\t\t\tver := procInst(\"version\", content)\n\t\t\tif ver != \"\" && ver != \"1.0\" {\n\t\t\t\td.err = fmt.Errorf(\"xml: unsupported version %q; only version 1.0 is supported\", ver)\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\tenc := procInst(\"encoding\", content)\n\t\t\tif enc != \"\" && enc != \"utf-8\" && enc != \"UTF-8\" {\n\t\t\t\tif d.CharsetReader == nil {\n\t\t\t\t\td.err = fmt.Errorf(\"xml: encoding %q declared but Decoder.CharsetReader is nil\", enc)\n\t\t\t\t\treturn nil, d.err\n\t\t\t\t}\n\t\t\t\tnewr, err := d.CharsetReader(enc, d.r.(io.Reader))\n\t\t\t\tif err != nil {\n\t\t\t\t\td.err = fmt.Errorf(\"xml: opening charset %q: %v\", enc, err)\n\t\t\t\t\treturn nil, d.err\n\t\t\t\t}\n\t\t\t\tif newr == nil {\n\t\t\t\t\tpanic(\"CharsetReader returned a nil Reader for charset \" + enc)\n\t\t\t\t}\n\t\t\t\td.switchToReader(newr)\n\t\t\t}\n\t\t}\n\t\treturn ProcInst{target, data}, nil\n\n\tcase '!':\n\t\t// <!: Maybe comment, maybe CDATA.\n\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\treturn nil, d.err\n\t\t}\n\t\tswitch b {\n\t\tcase '-': // <!-\n\t\t\t// Probably <!-- for a comment.\n\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\tif b != '-' {\n\t\t\t\td.err = d.syntaxError(\"invalid sequence <!- not part of <!--\")\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\t// Look for terminator.\n\t\t\td.buf.Reset()\n\t\t\tvar b0, b1 byte\n\t\t\tfor {\n\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\treturn nil, d.err\n\t\t\t\t}\n\t\t\t\td.buf.WriteByte(b)\n\t\t\t\tif b0 == '-' && b1 == '-' && b == '>' {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tb0, b1 = b1, b\n\t\t\t}\n\t\t\tdata := d.buf.Bytes()\n\t\t\tdata = data[0 : len(data)-3] // chop -->\n\t\t\treturn Comment(data), nil\n\n\t\tcase '[': // <![\n\t\t\t// Probably <![CDATA[.\n\t\t\tfor i := 0; i < 6; i++ {\n\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\treturn nil, d.err\n\t\t\t\t}\n\t\t\t\tif b != \"CDATA[\"[i] {\n\t\t\t\t\td.err = d.syntaxError(\"invalid <![ sequence\")\n\t\t\t\t\treturn nil, d.err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Have <![CDATA[.  Read text until ]]>.\n\t\t\tdata := d.text(-1, true)\n\t\t\tif data == nil {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\treturn CharData(data), nil\n\t\t}\n\n\t\t// Probably a directive: <!DOCTYPE ...>, <!ENTITY ...>, etc.\n\t\t// We don't care, but accumulate for caller. Quoted angle\n\t\t// brackets do not count for nesting.\n\t\td.buf.Reset()\n\t\td.buf.WriteByte(b)\n\t\tinquote := uint8(0)\n\t\tdepth := 0\n\t\tfor {\n\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\tif inquote == 0 && b == '>' && depth == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\tHandleB:\n\t\t\td.buf.WriteByte(b)\n\t\t\tswitch {\n\t\t\tcase b == inquote:\n\t\t\t\tinquote = 0\n\n\t\t\tcase inquote != 0:\n\t\t\t\t// in quotes, no special action\n\n\t\t\tcase b == '\\'' || b == '\"':\n\t\t\t\tinquote = b\n\n\t\t\tcase b == '>' && inquote == 0:\n\t\t\t\tdepth--\n\n\t\t\tcase b == '<' && inquote == 0:\n\t\t\t\t// Look for <!-- to begin comment.\n\t\t\t\ts := \"!--\"\n\t\t\t\tfor i := 0; i < len(s); i++ {\n\t\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\t\treturn nil, d.err\n\t\t\t\t\t}\n\t\t\t\t\tif b != s[i] {\n\t\t\t\t\t\tfor j := 0; j < i; j++ {\n\t\t\t\t\t\t\td.buf.WriteByte(s[j])\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdepth++\n\t\t\t\t\t\tgoto HandleB\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Remove < that was written above.\n\t\t\t\td.buf.Truncate(d.buf.Len() - 1)\n\n\t\t\t\t// Look for terminator.\n\t\t\t\tvar b0, b1 byte\n\t\t\t\tfor {\n\t\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\t\treturn nil, d.err\n\t\t\t\t\t}\n\t\t\t\t\tif b0 == '-' && b1 == '-' && b == '>' {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tb0, b1 = b1, b\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Directive(d.buf.Bytes()), nil\n\t}\n\n\t// Must be an open element like <a href=\"foo\">\n\td.ungetc(b)\n\n\tvar (\n\t\tname  Name\n\t\tempty bool\n\t\tattr  []Attr\n\t)\n\tif name, ok = d.nsname(); !ok {\n\t\tif d.err == nil {\n\t\t\td.err = d.syntaxError(\"expected element name after <\")\n\t\t}\n\t\treturn nil, d.err\n\t}\n\n\tattr = []Attr{}\n\tfor {\n\t\td.space()\n\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\treturn nil, d.err\n\t\t}\n\t\tif b == '/' {\n\t\t\tempty = true\n\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\tif b != '>' {\n\t\t\t\td.err = d.syntaxError(\"expected /> in element\")\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif b == '>' {\n\t\t\tbreak\n\t\t}\n\t\td.ungetc(b)\n\n\t\tn := len(attr)\n\t\tif n >= cap(attr) {\n\t\t\tnCap := 2 * cap(attr)\n\t\t\tif nCap == 0 {\n\t\t\t\tnCap = 4\n\t\t\t}\n\t\t\tnattr := make([]Attr, n, nCap)\n\t\t\tcopy(nattr, attr)\n\t\t\tattr = nattr\n\t\t}\n\t\tattr = attr[0 : n+1]\n\t\ta := &attr[n]\n\t\tif a.Name, ok = d.nsname(); !ok {\n\t\t\tif d.err == nil {\n\t\t\t\td.err = d.syntaxError(\"expected attribute name in element\")\n\t\t\t}\n\t\t\treturn nil, d.err\n\t\t}\n\t\td.space()\n\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\treturn nil, d.err\n\t\t}\n\t\tif b != '=' {\n\t\t\tif d.Strict {\n\t\t\t\td.err = d.syntaxError(\"attribute name without = in element\")\n\t\t\t\treturn nil, d.err\n\t\t\t} else {\n\t\t\t\td.ungetc(b)\n\t\t\t\ta.Value = a.Name.Local\n\t\t\t}\n\t\t} else {\n\t\t\td.space()\n\t\t\tdata := d.attrval()\n\t\t\tif data == nil {\n\t\t\t\treturn nil, d.err\n\t\t\t}\n\t\t\ta.Value = string(data)\n\t\t}\n\t}\n\tif empty {\n\t\td.needClose = true\n\t\td.toClose = name\n\t}\n\treturn StartElement{name, attr}, nil\n}\n\nfunc (d *Decoder) attrval() []byte {\n\tb, ok := d.mustgetc()\n\tif !ok {\n\t\treturn nil\n\t}\n\t// Handle quoted attribute values\n\tif b == '\"' || b == '\\'' {\n\t\treturn d.text(int(b), false)\n\t}\n\t// Handle unquoted attribute values for strict parsers\n\tif d.Strict {\n\t\td.err = d.syntaxError(\"unquoted or missing attribute value in element\")\n\t\treturn nil\n\t}\n\t// Handle unquoted attribute values for unstrict parsers\n\td.ungetc(b)\n\td.buf.Reset()\n\tfor {\n\t\tb, ok = d.mustgetc()\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\t// http://www.w3.org/TR/REC-html40/intro/sgmltut.html#h-3.2.2\n\t\tif 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z' ||\n\t\t\t'0' <= b && b <= '9' || b == '_' || b == ':' || b == '-' {\n\t\t\td.buf.WriteByte(b)\n\t\t} else {\n\t\t\td.ungetc(b)\n\t\t\tbreak\n\t\t}\n\t}\n\treturn d.buf.Bytes()\n}\n\n// Skip spaces if any\nfunc (d *Decoder) space() {\n\tfor {\n\t\tb, ok := d.getc()\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\tswitch b {\n\t\tcase ' ', '\\r', '\\n', '\\t':\n\t\tdefault:\n\t\t\td.ungetc(b)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Read a single byte.\n// If there is no byte to read, return ok==false\n// and leave the error in d.err.\n// Maintain line number.\nfunc (d *Decoder) getc() (b byte, ok bool) {\n\tif d.err != nil {\n\t\treturn 0, false\n\t}\n\tif d.nextByte >= 0 {\n\t\tb = byte(d.nextByte)\n\t\td.nextByte = -1\n\t} else {\n\t\tb, d.err = d.r.ReadByte()\n\t\tif d.err != nil {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d.saved != nil {\n\t\t\td.saved.WriteByte(b)\n\t\t}\n\t}\n\tif b == '\\n' {\n\t\td.line++\n\t}\n\td.offset++\n\treturn b, true\n}\n\n// InputOffset returns the input stream byte offset of the current decoder position.\n// The offset gives the location of the end of the most recently returned token\n// and the beginning of the next token.\nfunc (d *Decoder) InputOffset() int64 {\n\treturn d.offset\n}\n\n// Return saved offset.\n// If we did ungetc (nextByte >= 0), have to back up one.\nfunc (d *Decoder) savedOffset() int {\n\tn := d.saved.Len()\n\tif d.nextByte >= 0 {\n\t\tn--\n\t}\n\treturn n\n}\n\n// Must read a single byte.\n// If there is no byte to read,\n// set d.err to SyntaxError(\"unexpected EOF\")\n// and return ok==false\nfunc (d *Decoder) mustgetc() (b byte, ok bool) {\n\tif b, ok = d.getc(); !ok {\n\t\tif d.err == io.EOF {\n\t\t\td.err = d.syntaxError(\"unexpected EOF\")\n\t\t}\n\t}\n\treturn\n}\n\n// Unread a single byte.\nfunc (d *Decoder) ungetc(b byte) {\n\tif b == '\\n' {\n\t\td.line--\n\t}\n\td.nextByte = int(b)\n\td.offset--\n}\n\nvar entity = map[string]rune{\n\t\"lt\":   '<',\n\t\"gt\":   '>',\n\t\"amp\":  '&',\n\t\"apos\": '\\'',\n\t\"quot\": '\"',\n}\n\n// Read plain text section (XML calls it character data).\n// If quote >= 0, we are in a quoted string and need to find the matching quote.\n// If cdata == true, we are in a <![CDATA[ section and need to find ]]>.\n// On failure return nil and leave the error in d.err.\nfunc (d *Decoder) text(quote int, cdata bool) []byte {\n\tvar b0, b1 byte\n\tvar trunc int\n\td.buf.Reset()\nInput:\n\tfor {\n\t\tb, ok := d.getc()\n\t\tif !ok {\n\t\t\tif cdata {\n\t\t\t\tif d.err == io.EOF {\n\t\t\t\t\td.err = d.syntaxError(\"unexpected EOF in CDATA section\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbreak Input\n\t\t}\n\n\t\t// <![CDATA[ section ends with ]]>.\n\t\t// It is an error for ]]> to appear in ordinary text.\n\t\tif b0 == ']' && b1 == ']' && b == '>' {\n\t\t\tif cdata {\n\t\t\t\ttrunc = 2\n\t\t\t\tbreak Input\n\t\t\t}\n\t\t\td.err = d.syntaxError(\"unescaped ]]> not in CDATA section\")\n\t\t\treturn nil\n\t\t}\n\n\t\t// Stop reading text if we see a <.\n\t\tif b == '<' && !cdata {\n\t\t\tif quote >= 0 {\n\t\t\t\td.err = d.syntaxError(\"unescaped < inside quoted string\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\td.ungetc('<')\n\t\t\tbreak Input\n\t\t}\n\t\tif quote >= 0 && b == byte(quote) {\n\t\t\tbreak Input\n\t\t}\n\t\tif b == '&' && !cdata {\n\t\t\t// Read escaped character expression up to semicolon.\n\t\t\t// XML in all its glory allows a document to define and use\n\t\t\t// its own character names with <!ENTITY ...> directives.\n\t\t\t// Parsers are required to recognize lt, gt, amp, apos, and quot\n\t\t\t// even if they have not been declared.\n\t\t\tbefore := d.buf.Len()\n\t\t\td.buf.WriteByte('&')\n\t\t\tvar ok bool\n\t\t\tvar text string\n\t\t\tvar haveText bool\n\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif b == '#' {\n\t\t\t\td.buf.WriteByte(b)\n\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tbase := 10\n\t\t\t\tif b == 'x' {\n\t\t\t\t\tbase = 16\n\t\t\t\t\td.buf.WriteByte(b)\n\t\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstart := d.buf.Len()\n\t\t\t\tfor '0' <= b && b <= '9' ||\n\t\t\t\t\tbase == 16 && 'a' <= b && b <= 'f' ||\n\t\t\t\t\tbase == 16 && 'A' <= b && b <= 'F' {\n\t\t\t\t\td.buf.WriteByte(b)\n\t\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif b != ';' {\n\t\t\t\t\td.ungetc(b)\n\t\t\t\t} else {\n\t\t\t\t\ts := string(d.buf.Bytes()[start:])\n\t\t\t\t\td.buf.WriteByte(';')\n\t\t\t\t\tn, err := strconv.ParseUint(s, base, 64)\n\t\t\t\t\tif err == nil && n <= unicode.MaxRune {\n\t\t\t\t\t\ttext = string(rune(n))\n\t\t\t\t\t\thaveText = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\td.ungetc(b)\n\t\t\t\tif !d.readName() {\n\t\t\t\t\tif d.err != nil {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tok = false\n\t\t\t\t}\n\t\t\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tif b != ';' {\n\t\t\t\t\td.ungetc(b)\n\t\t\t\t} else {\n\t\t\t\t\tname := d.buf.Bytes()[before+1:]\n\t\t\t\t\td.buf.WriteByte(';')\n\t\t\t\t\tif isName(name) {\n\t\t\t\t\t\ts := string(name)\n\t\t\t\t\t\tif r, ok := entity[s]; ok {\n\t\t\t\t\t\t\ttext = string(r)\n\t\t\t\t\t\t\thaveText = true\n\t\t\t\t\t\t} else if d.Entity != nil {\n\t\t\t\t\t\t\ttext, haveText = d.Entity[s]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif haveText {\n\t\t\t\td.buf.Truncate(before)\n\t\t\t\td.buf.Write([]byte(text))\n\t\t\t\tb0, b1 = 0, 0\n\t\t\t\tcontinue Input\n\t\t\t}\n\t\t\tif !d.Strict {\n\t\t\t\tb0, b1 = 0, 0\n\t\t\t\tcontinue Input\n\t\t\t}\n\t\t\tent := string(d.buf.Bytes()[before:])\n\t\t\tif ent[len(ent)-1] != ';' {\n\t\t\t\tent += \" (no semicolon)\"\n\t\t\t}\n\t\t\td.err = d.syntaxError(\"invalid character entity \" + ent)\n\t\t\treturn nil\n\t\t}\n\n\t\t// We must rewrite unescaped \\r and \\r\\n into \\n.\n\t\tif b == '\\r' {\n\t\t\td.buf.WriteByte('\\n')\n\t\t} else if b1 == '\\r' && b == '\\n' {\n\t\t\t// Skip \\r\\n--we already wrote \\n.\n\t\t} else {\n\t\t\td.buf.WriteByte(b)\n\t\t}\n\n\t\tb0, b1 = b1, b\n\t}\n\tdata := d.buf.Bytes()\n\tdata = data[0 : len(data)-trunc]\n\n\t// Inspect each rune for being a disallowed character.\n\tbuf := data\n\tfor len(buf) > 0 {\n\t\tr, size := utf8.DecodeRune(buf)\n\t\tif r == utf8.RuneError && size == 1 {\n\t\t\td.err = d.syntaxError(\"invalid UTF-8\")\n\t\t\treturn nil\n\t\t}\n\t\tbuf = buf[size:]\n\t\tif !isInCharacterRange(r) {\n\t\t\td.err = d.syntaxError(fmt.Sprintf(\"illegal character code %U\", r))\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn data\n}\n\n// Decide whether the given rune is in the XML Character Range, per\n// the Char production of http://www.xml.com/axml/testaxml.htm,\n// Section 2.2 Characters.\nfunc isInCharacterRange(r rune) (inrange bool) {\n\treturn r == 0x09 ||\n\t\tr == 0x0A ||\n\t\tr == 0x0D ||\n\t\tr >= 0x20 && r <= 0xDF77 ||\n\t\tr >= 0xE000 && r <= 0xFFFD ||\n\t\tr >= 0x10000 && r <= 0x10FFFF\n}\n\n// Get name space name: name with a : stuck in the middle.\n// The part before the : is the name space identifier.\nfunc (d *Decoder) nsname() (name Name, ok bool) {\n\ts, ok := d.name()\n\tif !ok {\n\t\treturn\n\t}\n\ti := strings.Index(s, \":\")\n\tif i < 0 {\n\t\tname.Local = s\n\t} else {\n\t\tname.Space = s[0:i]\n\t\tname.Local = s[i+1:]\n\t}\n\treturn name, true\n}\n\n// Get name: /first(first|second)*/\n// Do not set d.err if the name is missing (unless unexpected EOF is received):\n// let the caller provide better context.\nfunc (d *Decoder) name() (s string, ok bool) {\n\td.buf.Reset()\n\tif !d.readName() {\n\t\treturn \"\", false\n\t}\n\n\t// Now we check the characters.\n\tb := d.buf.Bytes()\n\tif !isName(b) {\n\t\td.err = d.syntaxError(\"invalid XML name: \" + string(b))\n\t\treturn \"\", false\n\t}\n\treturn string(b), true\n}\n\n// Read a name and append its bytes to d.buf.\n// The name is delimited by any single-byte character not valid in names.\n// All multi-byte characters are accepted; the caller must check their validity.\nfunc (d *Decoder) readName() (ok bool) {\n\tvar b byte\n\tif b, ok = d.mustgetc(); !ok {\n\t\treturn\n\t}\n\tif b < utf8.RuneSelf && !isNameByte(b) {\n\t\td.ungetc(b)\n\t\treturn false\n\t}\n\td.buf.WriteByte(b)\n\n\tfor {\n\t\tif b, ok = d.mustgetc(); !ok {\n\t\t\treturn\n\t\t}\n\t\tif b < utf8.RuneSelf && !isNameByte(b) {\n\t\t\td.ungetc(b)\n\t\t\tbreak\n\t\t}\n\t\td.buf.WriteByte(b)\n\t}\n\treturn true\n}\n\nfunc isNameByte(c byte) bool {\n\treturn 'A' <= c && c <= 'Z' ||\n\t\t'a' <= c && c <= 'z' ||\n\t\t'0' <= c && c <= '9' ||\n\t\tc == '_' || c == ':' || c == '.' || c == '-'\n}\n\nfunc isName(s []byte) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\tc, n := utf8.DecodeRune(s)\n\tif c == utf8.RuneError && n == 1 {\n\t\treturn false\n\t}\n\tif !unicode.Is(first, c) {\n\t\treturn false\n\t}\n\tfor n < len(s) {\n\t\ts = s[n:]\n\t\tc, n = utf8.DecodeRune(s)\n\t\tif c == utf8.RuneError && n == 1 {\n\t\t\treturn false\n\t\t}\n\t\tif !unicode.Is(first, c) && !unicode.Is(second, c) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isNameString(s string) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\tc, n := utf8.DecodeRuneInString(s)\n\tif c == utf8.RuneError && n == 1 {\n\t\treturn false\n\t}\n\tif !unicode.Is(first, c) {\n\t\treturn false\n\t}\n\tfor n < len(s) {\n\t\ts = s[n:]\n\t\tc, n = utf8.DecodeRuneInString(s)\n\t\tif c == utf8.RuneError && n == 1 {\n\t\t\treturn false\n\t\t}\n\t\tif !unicode.Is(first, c) && !unicode.Is(second, c) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// These tables were generated by cut and paste from Appendix B of\n// the XML spec at http://www.xml.com/axml/testaxml.htm\n// and then reformatting. First corresponds to (Letter | '_' | ':')\n// and second corresponds to NameChar.\n\nvar first = &unicode.RangeTable{\n\tR16: []unicode.Range16{\n\t\t{0x003A, 0x003A, 1},\n\t\t{0x0041, 0x005A, 1},\n\t\t{0x005F, 0x005F, 1},\n\t\t{0x0061, 0x007A, 1},\n\t\t{0x00C0, 0x00D6, 1},\n\t\t{0x00D8, 0x00F6, 1},\n\t\t{0x00F8, 0x00FF, 1},\n\t\t{0x0100, 0x0131, 1},\n\t\t{0x0134, 0x013E, 1},\n\t\t{0x0141, 0x0148, 1},\n\t\t{0x014A, 0x017E, 1},\n\t\t{0x0180, 0x01C3, 1},\n\t\t{0x01CD, 0x01F0, 1},\n\t\t{0x01F4, 0x01F5, 1},\n\t\t{0x01FA, 0x0217, 1},\n\t\t{0x0250, 0x02A8, 1},\n\t\t{0x02BB, 0x02C1, 1},\n\t\t{0x0386, 0x0386, 1},\n\t\t{0x0388, 0x038A, 1},\n\t\t{0x038C, 0x038C, 1},\n\t\t{0x038E, 0x03A1, 1},\n\t\t{0x03A3, 0x03CE, 1},\n\t\t{0x03D0, 0x03D6, 1},\n\t\t{0x03DA, 0x03E0, 2},\n\t\t{0x03E2, 0x03F3, 1},\n\t\t{0x0401, 0x040C, 1},\n\t\t{0x040E, 0x044F, 1},\n\t\t{0x0451, 0x045C, 1},\n\t\t{0x045E, 0x0481, 1},\n\t\t{0x0490, 0x04C4, 1},\n\t\t{0x04C7, 0x04C8, 1},\n\t\t{0x04CB, 0x04CC, 1},\n\t\t{0x04D0, 0x04EB, 1},\n\t\t{0x04EE, 0x04F5, 1},\n\t\t{0x04F8, 0x04F9, 1},\n\t\t{0x0531, 0x0556, 1},\n\t\t{0x0559, 0x0559, 1},\n\t\t{0x0561, 0x0586, 1},\n\t\t{0x05D0, 0x05EA, 1},\n\t\t{0x05F0, 0x05F2, 1},\n\t\t{0x0621, 0x063A, 1},\n\t\t{0x0641, 0x064A, 1},\n\t\t{0x0671, 0x06B7, 1},\n\t\t{0x06BA, 0x06BE, 1},\n\t\t{0x06C0, 0x06CE, 1},\n\t\t{0x06D0, 0x06D3, 1},\n\t\t{0x06D5, 0x06D5, 1},\n\t\t{0x06E5, 0x06E6, 1},\n\t\t{0x0905, 0x0939, 1},\n\t\t{0x093D, 0x093D, 1},\n\t\t{0x0958, 0x0961, 1},\n\t\t{0x0985, 0x098C, 1},\n\t\t{0x098F, 0x0990, 1},\n\t\t{0x0993, 0x09A8, 1},\n\t\t{0x09AA, 0x09B0, 1},\n\t\t{0x09B2, 0x09B2, 1},\n\t\t{0x09B6, 0x09B9, 1},\n\t\t{0x09DC, 0x09DD, 1},\n\t\t{0x09DF, 0x09E1, 1},\n\t\t{0x09F0, 0x09F1, 1},\n\t\t{0x0A05, 0x0A0A, 1},\n\t\t{0x0A0F, 0x0A10, 1},\n\t\t{0x0A13, 0x0A28, 1},\n\t\t{0x0A2A, 0x0A30, 1},\n\t\t{0x0A32, 0x0A33, 1},\n\t\t{0x0A35, 0x0A36, 1},\n\t\t{0x0A38, 0x0A39, 1},\n\t\t{0x0A59, 0x0A5C, 1},\n\t\t{0x0A5E, 0x0A5E, 1},\n\t\t{0x0A72, 0x0A74, 1},\n\t\t{0x0A85, 0x0A8B, 1},\n\t\t{0x0A8D, 0x0A8D, 1},\n\t\t{0x0A8F, 0x0A91, 1},\n\t\t{0x0A93, 0x0AA8, 1},\n\t\t{0x0AAA, 0x0AB0, 1},\n\t\t{0x0AB2, 0x0AB3, 1},\n\t\t{0x0AB5, 0x0AB9, 1},\n\t\t{0x0ABD, 0x0AE0, 0x23},\n\t\t{0x0B05, 0x0B0C, 1},\n\t\t{0x0B0F, 0x0B10, 1},\n\t\t{0x0B13, 0x0B28, 1},\n\t\t{0x0B2A, 0x0B30, 1},\n\t\t{0x0B32, 0x0B33, 1},\n\t\t{0x0B36, 0x0B39, 1},\n\t\t{0x0B3D, 0x0B3D, 1},\n\t\t{0x0B5C, 0x0B5D, 1},\n\t\t{0x0B5F, 0x0B61, 1},\n\t\t{0x0B85, 0x0B8A, 1},\n\t\t{0x0B8E, 0x0B90, 1},\n\t\t{0x0B92, 0x0B95, 1},\n\t\t{0x0B99, 0x0B9A, 1},\n\t\t{0x0B9C, 0x0B9C, 1},\n\t\t{0x0B9E, 0x0B9F, 1},\n\t\t{0x0BA3, 0x0BA4, 1},\n\t\t{0x0BA8, 0x0BAA, 1},\n\t\t{0x0BAE, 0x0BB5, 1},\n\t\t{0x0BB7, 0x0BB9, 1},\n\t\t{0x0C05, 0x0C0C, 1},\n\t\t{0x0C0E, 0x0C10, 1},\n\t\t{0x0C12, 0x0C28, 1},\n\t\t{0x0C2A, 0x0C33, 1},\n\t\t{0x0C35, 0x0C39, 1},\n\t\t{0x0C60, 0x0C61, 1},\n\t\t{0x0C85, 0x0C8C, 1},\n\t\t{0x0C8E, 0x0C90, 1},\n\t\t{0x0C92, 0x0CA8, 1},\n\t\t{0x0CAA, 0x0CB3, 1},\n\t\t{0x0CB5, 0x0CB9, 1},\n\t\t{0x0CDE, 0x0CDE, 1},\n\t\t{0x0CE0, 0x0CE1, 1},\n\t\t{0x0D05, 0x0D0C, 1},\n\t\t{0x0D0E, 0x0D10, 1},\n\t\t{0x0D12, 0x0D28, 1},\n\t\t{0x0D2A, 0x0D39, 1},\n\t\t{0x0D60, 0x0D61, 1},\n\t\t{0x0E01, 0x0E2E, 1},\n\t\t{0x0E30, 0x0E30, 1},\n\t\t{0x0E32, 0x0E33, 1},\n\t\t{0x0E40, 0x0E45, 1},\n\t\t{0x0E81, 0x0E82, 1},\n\t\t{0x0E84, 0x0E84, 1},\n\t\t{0x0E87, 0x0E88, 1},\n\t\t{0x0E8A, 0x0E8D, 3},\n\t\t{0x0E94, 0x0E97, 1},\n\t\t{0x0E99, 0x0E9F, 1},\n\t\t{0x0EA1, 0x0EA3, 1},\n\t\t{0x0EA5, 0x0EA7, 2},\n\t\t{0x0EAA, 0x0EAB, 1},\n\t\t{0x0EAD, 0x0EAE, 1},\n\t\t{0x0EB0, 0x0EB0, 1},\n\t\t{0x0EB2, 0x0EB3, 1},\n\t\t{0x0EBD, 0x0EBD, 1},\n\t\t{0x0EC0, 0x0EC4, 1},\n\t\t{0x0F40, 0x0F47, 1},\n\t\t{0x0F49, 0x0F69, 1},\n\t\t{0x10A0, 0x10C5, 1},\n\t\t{0x10D0, 0x10F6, 1},\n\t\t{0x1100, 0x1100, 1},\n\t\t{0x1102, 0x1103, 1},\n\t\t{0x1105, 0x1107, 1},\n\t\t{0x1109, 0x1109, 1},\n\t\t{0x110B, 0x110C, 1},\n\t\t{0x110E, 0x1112, 1},\n\t\t{0x113C, 0x1140, 2},\n\t\t{0x114C, 0x1150, 2},\n\t\t{0x1154, 0x1155, 1},\n\t\t{0x1159, 0x1159, 1},\n\t\t{0x115F, 0x1161, 1},\n\t\t{0x1163, 0x1169, 2},\n\t\t{0x116D, 0x116E, 1},\n\t\t{0x1172, 0x1173, 1},\n\t\t{0x1175, 0x119E, 0x119E - 0x1175},\n\t\t{0x11A8, 0x11AB, 0x11AB - 0x11A8},\n\t\t{0x11AE, 0x11AF, 1},\n\t\t{0x11B7, 0x11B8, 1},\n\t\t{0x11BA, 0x11BA, 1},\n\t\t{0x11BC, 0x11C2, 1},\n\t\t{0x11EB, 0x11F0, 0x11F0 - 0x11EB},\n\t\t{0x11F9, 0x11F9, 1},\n\t\t{0x1E00, 0x1E9B, 1},\n\t\t{0x1EA0, 0x1EF9, 1},\n\t\t{0x1F00, 0x1F15, 1},\n\t\t{0x1F18, 0x1F1D, 1},\n\t\t{0x1F20, 0x1F45, 1},\n\t\t{0x1F48, 0x1F4D, 1},\n\t\t{0x1F50, 0x1F57, 1},\n\t\t{0x1F59, 0x1F5B, 0x1F5B - 0x1F59},\n\t\t{0x1F5D, 0x1F5D, 1},\n\t\t{0x1F5F, 0x1F7D, 1},\n\t\t{0x1F80, 0x1FB4, 1},\n\t\t{0x1FB6, 0x1FBC, 1},\n\t\t{0x1FBE, 0x1FBE, 1},\n\t\t{0x1FC2, 0x1FC4, 1},\n\t\t{0x1FC6, 0x1FCC, 1},\n\t\t{0x1FD0, 0x1FD3, 1},\n\t\t{0x1FD6, 0x1FDB, 1},\n\t\t{0x1FE0, 0x1FEC, 1},\n\t\t{0x1FF2, 0x1FF4, 1},\n\t\t{0x1FF6, 0x1FFC, 1},\n\t\t{0x2126, 0x2126, 1},\n\t\t{0x212A, 0x212B, 1},\n\t\t{0x212E, 0x212E, 1},\n\t\t{0x2180, 0x2182, 1},\n\t\t{0x3007, 0x3007, 1},\n\t\t{0x3021, 0x3029, 1},\n\t\t{0x3041, 0x3094, 1},\n\t\t{0x30A1, 0x30FA, 1},\n\t\t{0x3105, 0x312C, 1},\n\t\t{0x4E00, 0x9FA5, 1},\n\t\t{0xAC00, 0xD7A3, 1},\n\t},\n}\n\nvar second = &unicode.RangeTable{\n\tR16: []unicode.Range16{\n\t\t{0x002D, 0x002E, 1},\n\t\t{0x0030, 0x0039, 1},\n\t\t{0x00B7, 0x00B7, 1},\n\t\t{0x02D0, 0x02D1, 1},\n\t\t{0x0300, 0x0345, 1},\n\t\t{0x0360, 0x0361, 1},\n\t\t{0x0387, 0x0387, 1},\n\t\t{0x0483, 0x0486, 1},\n\t\t{0x0591, 0x05A1, 1},\n\t\t{0x05A3, 0x05B9, 1},\n\t\t{0x05BB, 0x05BD, 1},\n\t\t{0x05BF, 0x05BF, 1},\n\t\t{0x05C1, 0x05C2, 1},\n\t\t{0x05C4, 0x0640, 0x0640 - 0x05C4},\n\t\t{0x064B, 0x0652, 1},\n\t\t{0x0660, 0x0669, 1},\n\t\t{0x0670, 0x0670, 1},\n\t\t{0x06D6, 0x06DC, 1},\n\t\t{0x06DD, 0x06DF, 1},\n\t\t{0x06E0, 0x06E4, 1},\n\t\t{0x06E7, 0x06E8, 1},\n\t\t{0x06EA, 0x06ED, 1},\n\t\t{0x06F0, 0x06F9, 1},\n\t\t{0x0901, 0x0903, 1},\n\t\t{0x093C, 0x093C, 1},\n\t\t{0x093E, 0x094C, 1},\n\t\t{0x094D, 0x094D, 1},\n\t\t{0x0951, 0x0954, 1},\n\t\t{0x0962, 0x0963, 1},\n\t\t{0x0966, 0x096F, 1},\n\t\t{0x0981, 0x0983, 1},\n\t\t{0x09BC, 0x09BC, 1},\n\t\t{0x09BE, 0x09BF, 1},\n\t\t{0x09C0, 0x09C4, 1},\n\t\t{0x09C7, 0x09C8, 1},\n\t\t{0x09CB, 0x09CD, 1},\n\t\t{0x09D7, 0x09D7, 1},\n\t\t{0x09E2, 0x09E3, 1},\n\t\t{0x09E6, 0x09EF, 1},\n\t\t{0x0A02, 0x0A3C, 0x3A},\n\t\t{0x0A3E, 0x0A3F, 1},\n\t\t{0x0A40, 0x0A42, 1},\n\t\t{0x0A47, 0x0A48, 1},\n\t\t{0x0A4B, 0x0A4D, 1},\n\t\t{0x0A66, 0x0A6F, 1},\n\t\t{0x0A70, 0x0A71, 1},\n\t\t{0x0A81, 0x0A83, 1},\n\t\t{0x0ABC, 0x0ABC, 1},\n\t\t{0x0ABE, 0x0AC5, 1},\n\t\t{0x0AC7, 0x0AC9, 1},\n\t\t{0x0ACB, 0x0ACD, 1},\n\t\t{0x0AE6, 0x0AEF, 1},\n\t\t{0x0B01, 0x0B03, 1},\n\t\t{0x0B3C, 0x0B3C, 1},\n\t\t{0x0B3E, 0x0B43, 1},\n\t\t{0x0B47, 0x0B48, 1},\n\t\t{0x0B4B, 0x0B4D, 1},\n\t\t{0x0B56, 0x0B57, 1},\n\t\t{0x0B66, 0x0B6F, 1},\n\t\t{0x0B82, 0x0B83, 1},\n\t\t{0x0BBE, 0x0BC2, 1},\n\t\t{0x0BC6, 0x0BC8, 1},\n\t\t{0x0BCA, 0x0BCD, 1},\n\t\t{0x0BD7, 0x0BD7, 1},\n\t\t{0x0BE7, 0x0BEF, 1},\n\t\t{0x0C01, 0x0C03, 1},\n\t\t{0x0C3E, 0x0C44, 1},\n\t\t{0x0C46, 0x0C48, 1},\n\t\t{0x0C4A, 0x0C4D, 1},\n\t\t{0x0C55, 0x0C56, 1},\n\t\t{0x0C66, 0x0C6F, 1},\n\t\t{0x0C82, 0x0C83, 1},\n\t\t{0x0CBE, 0x0CC4, 1},\n\t\t{0x0CC6, 0x0CC8, 1},\n\t\t{0x0CCA, 0x0CCD, 1},\n\t\t{0x0CD5, 0x0CD6, 1},\n\t\t{0x0CE6, 0x0CEF, 1},\n\t\t{0x0D02, 0x0D03, 1},\n\t\t{0x0D3E, 0x0D43, 1},\n\t\t{0x0D46, 0x0D48, 1},\n\t\t{0x0D4A, 0x0D4D, 1},\n\t\t{0x0D57, 0x0D57, 1},\n\t\t{0x0D66, 0x0D6F, 1},\n\t\t{0x0E31, 0x0E31, 1},\n\t\t{0x0E34, 0x0E3A, 1},\n\t\t{0x0E46, 0x0E46, 1},\n\t\t{0x0E47, 0x0E4E, 1},\n\t\t{0x0E50, 0x0E59, 1},\n\t\t{0x0EB1, 0x0EB1, 1},\n\t\t{0x0EB4, 0x0EB9, 1},\n\t\t{0x0EBB, 0x0EBC, 1},\n\t\t{0x0EC6, 0x0EC6, 1},\n\t\t{0x0EC8, 0x0ECD, 1},\n\t\t{0x0ED0, 0x0ED9, 1},\n\t\t{0x0F18, 0x0F19, 1},\n\t\t{0x0F20, 0x0F29, 1},\n\t\t{0x0F35, 0x0F39, 2},\n\t\t{0x0F3E, 0x0F3F, 1},\n\t\t{0x0F71, 0x0F84, 1},\n\t\t{0x0F86, 0x0F8B, 1},\n\t\t{0x0F90, 0x0F95, 1},\n\t\t{0x0F97, 0x0F97, 1},\n\t\t{0x0F99, 0x0FAD, 1},\n\t\t{0x0FB1, 0x0FB7, 1},\n\t\t{0x0FB9, 0x0FB9, 1},\n\t\t{0x20D0, 0x20DC, 1},\n\t\t{0x20E1, 0x3005, 0x3005 - 0x20E1},\n\t\t{0x302A, 0x302F, 1},\n\t\t{0x3031, 0x3035, 1},\n\t\t{0x3099, 0x309A, 1},\n\t\t{0x309D, 0x309E, 1},\n\t\t{0x30FC, 0x30FE, 1},\n\t},\n}\n\n// HTMLEntity is an entity map containing translations for the\n// standard HTML entity characters.\nvar HTMLEntity = htmlEntity\n\nvar htmlEntity = map[string]string{\n\t/*\n\t\thget http://www.w3.org/TR/html4/sgml/entities.html |\n\t\tssam '\n\t\t\t,y /\\&gt;/ x/\\&lt;(.|\\n)+/ s/\\n/ /g\n\t\t\t,x v/^\\&lt;!ENTITY/d\n\t\t\t,s/\\&lt;!ENTITY ([^ ]+) .*U\\+([0-9A-F][0-9A-F][0-9A-F][0-9A-F]) .+/\t\"\\1\": \"\\\\u\\2\",/g\n\t\t'\n\t*/\n\t\"nbsp\":     \"\\u00A0\",\n\t\"iexcl\":    \"\\u00A1\",\n\t\"cent\":     \"\\u00A2\",\n\t\"pound\":    \"\\u00A3\",\n\t\"curren\":   \"\\u00A4\",\n\t\"yen\":      \"\\u00A5\",\n\t\"brvbar\":   \"\\u00A6\",\n\t\"sect\":     \"\\u00A7\",\n\t\"uml\":      \"\\u00A8\",\n\t\"copy\":     \"\\u00A9\",\n\t\"ordf\":     \"\\u00AA\",\n\t\"laquo\":    \"\\u00AB\",\n\t\"not\":      \"\\u00AC\",\n\t\"shy\":      \"\\u00AD\",\n\t\"reg\":      \"\\u00AE\",\n\t\"macr\":     \"\\u00AF\",\n\t\"deg\":      \"\\u00B0\",\n\t\"plusmn\":   \"\\u00B1\",\n\t\"sup2\":     \"\\u00B2\",\n\t\"sup3\":     \"\\u00B3\",\n\t\"acute\":    \"\\u00B4\",\n\t\"micro\":    \"\\u00B5\",\n\t\"para\":     \"\\u00B6\",\n\t\"middot\":   \"\\u00B7\",\n\t\"cedil\":    \"\\u00B8\",\n\t\"sup1\":     \"\\u00B9\",\n\t\"ordm\":     \"\\u00BA\",\n\t\"raquo\":    \"\\u00BB\",\n\t\"frac14\":   \"\\u00BC\",\n\t\"frac12\":   \"\\u00BD\",\n\t\"frac34\":   \"\\u00BE\",\n\t\"iquest\":   \"\\u00BF\",\n\t\"Agrave\":   \"\\u00C0\",\n\t\"Aacute\":   \"\\u00C1\",\n\t\"Acirc\":    \"\\u00C2\",\n\t\"Atilde\":   \"\\u00C3\",\n\t\"Auml\":     \"\\u00C4\",\n\t\"Aring\":    \"\\u00C5\",\n\t\"AElig\":    \"\\u00C6\",\n\t\"Ccedil\":   \"\\u00C7\",\n\t\"Egrave\":   \"\\u00C8\",\n\t\"Eacute\":   \"\\u00C9\",\n\t\"Ecirc\":    \"\\u00CA\",\n\t\"Euml\":     \"\\u00CB\",\n\t\"Igrave\":   \"\\u00CC\",\n\t\"Iacute\":   \"\\u00CD\",\n\t\"Icirc\":    \"\\u00CE\",\n\t\"Iuml\":     \"\\u00CF\",\n\t\"ETH\":      \"\\u00D0\",\n\t\"Ntilde\":   \"\\u00D1\",\n\t\"Ograve\":   \"\\u00D2\",\n\t\"Oacute\":   \"\\u00D3\",\n\t\"Ocirc\":    \"\\u00D4\",\n\t\"Otilde\":   \"\\u00D5\",\n\t\"Ouml\":     \"\\u00D6\",\n\t\"times\":    \"\\u00D7\",\n\t\"Oslash\":   \"\\u00D8\",\n\t\"Ugrave\":   \"\\u00D9\",\n\t\"Uacute\":   \"\\u00DA\",\n\t\"Ucirc\":    \"\\u00DB\",\n\t\"Uuml\":     \"\\u00DC\",\n\t\"Yacute\":   \"\\u00DD\",\n\t\"THORN\":    \"\\u00DE\",\n\t\"szlig\":    \"\\u00DF\",\n\t\"agrave\":   \"\\u00E0\",\n\t\"aacute\":   \"\\u00E1\",\n\t\"acirc\":    \"\\u00E2\",\n\t\"atilde\":   \"\\u00E3\",\n\t\"auml\":     \"\\u00E4\",\n\t\"aring\":    \"\\u00E5\",\n\t\"aelig\":    \"\\u00E6\",\n\t\"ccedil\":   \"\\u00E7\",\n\t\"egrave\":   \"\\u00E8\",\n\t\"eacute\":   \"\\u00E9\",\n\t\"ecirc\":    \"\\u00EA\",\n\t\"euml\":     \"\\u00EB\",\n\t\"igrave\":   \"\\u00EC\",\n\t\"iacute\":   \"\\u00ED\",\n\t\"icirc\":    \"\\u00EE\",\n\t\"iuml\":     \"\\u00EF\",\n\t\"eth\":      \"\\u00F0\",\n\t\"ntilde\":   \"\\u00F1\",\n\t\"ograve\":   \"\\u00F2\",\n\t\"oacute\":   \"\\u00F3\",\n\t\"ocirc\":    \"\\u00F4\",\n\t\"otilde\":   \"\\u00F5\",\n\t\"ouml\":     \"\\u00F6\",\n\t\"divide\":   \"\\u00F7\",\n\t\"oslash\":   \"\\u00F8\",\n\t\"ugrave\":   \"\\u00F9\",\n\t\"uacute\":   \"\\u00FA\",\n\t\"ucirc\":    \"\\u00FB\",\n\t\"uuml\":     \"\\u00FC\",\n\t\"yacute\":   \"\\u00FD\",\n\t\"thorn\":    \"\\u00FE\",\n\t\"yuml\":     \"\\u00FF\",\n\t\"fnof\":     \"\\u0192\",\n\t\"Alpha\":    \"\\u0391\",\n\t\"Beta\":     \"\\u0392\",\n\t\"Gamma\":    \"\\u0393\",\n\t\"Delta\":    \"\\u0394\",\n\t\"Epsilon\":  \"\\u0395\",\n\t\"Zeta\":     \"\\u0396\",\n\t\"Eta\":      \"\\u0397\",\n\t\"Theta\":    \"\\u0398\",\n\t\"Iota\":     \"\\u0399\",\n\t\"Kappa\":    \"\\u039A\",\n\t\"Lambda\":   \"\\u039B\",\n\t\"Mu\":       \"\\u039C\",\n\t\"Nu\":       \"\\u039D\",\n\t\"Xi\":       \"\\u039E\",\n\t\"Omicron\":  \"\\u039F\",\n\t\"Pi\":       \"\\u03A0\",\n\t\"Rho\":      \"\\u03A1\",\n\t\"Sigma\":    \"\\u03A3\",\n\t\"Tau\":      \"\\u03A4\",\n\t\"Upsilon\":  \"\\u03A5\",\n\t\"Phi\":      \"\\u03A6\",\n\t\"Chi\":      \"\\u03A7\",\n\t\"Psi\":      \"\\u03A8\",\n\t\"Omega\":    \"\\u03A9\",\n\t\"alpha\":    \"\\u03B1\",\n\t\"beta\":     \"\\u03B2\",\n\t\"gamma\":    \"\\u03B3\",\n\t\"delta\":    \"\\u03B4\",\n\t\"epsilon\":  \"\\u03B5\",\n\t\"zeta\":     \"\\u03B6\",\n\t\"eta\":      \"\\u03B7\",\n\t\"theta\":    \"\\u03B8\",\n\t\"iota\":     \"\\u03B9\",\n\t\"kappa\":    \"\\u03BA\",\n\t\"lambda\":   \"\\u03BB\",\n\t\"mu\":       \"\\u03BC\",\n\t\"nu\":       \"\\u03BD\",\n\t\"xi\":       \"\\u03BE\",\n\t\"omicron\":  \"\\u03BF\",\n\t\"pi\":       \"\\u03C0\",\n\t\"rho\":      \"\\u03C1\",\n\t\"sigmaf\":   \"\\u03C2\",\n\t\"sigma\":    \"\\u03C3\",\n\t\"tau\":      \"\\u03C4\",\n\t\"upsilon\":  \"\\u03C5\",\n\t\"phi\":      \"\\u03C6\",\n\t\"chi\":      \"\\u03C7\",\n\t\"psi\":      \"\\u03C8\",\n\t\"omega\":    \"\\u03C9\",\n\t\"thetasym\": \"\\u03D1\",\n\t\"upsih\":    \"\\u03D2\",\n\t\"piv\":      \"\\u03D6\",\n\t\"bull\":     \"\\u2022\",\n\t\"hellip\":   \"\\u2026\",\n\t\"prime\":    \"\\u2032\",\n\t\"Prime\":    \"\\u2033\",\n\t\"oline\":    \"\\u203E\",\n\t\"frasl\":    \"\\u2044\",\n\t\"weierp\":   \"\\u2118\",\n\t\"image\":    \"\\u2111\",\n\t\"real\":     \"\\u211C\",\n\t\"trade\":    \"\\u2122\",\n\t\"alefsym\":  \"\\u2135\",\n\t\"larr\":     \"\\u2190\",\n\t\"uarr\":     \"\\u2191\",\n\t\"rarr\":     \"\\u2192\",\n\t\"darr\":     \"\\u2193\",\n\t\"harr\":     \"\\u2194\",\n\t\"crarr\":    \"\\u21B5\",\n\t\"lArr\":     \"\\u21D0\",\n\t\"uArr\":     \"\\u21D1\",\n\t\"rArr\":     \"\\u21D2\",\n\t\"dArr\":     \"\\u21D3\",\n\t\"hArr\":     \"\\u21D4\",\n\t\"forall\":   \"\\u2200\",\n\t\"part\":     \"\\u2202\",\n\t\"exist\":    \"\\u2203\",\n\t\"empty\":    \"\\u2205\",\n\t\"nabla\":    \"\\u2207\",\n\t\"isin\":     \"\\u2208\",\n\t\"notin\":    \"\\u2209\",\n\t\"ni\":       \"\\u220B\",\n\t\"prod\":     \"\\u220F\",\n\t\"sum\":      \"\\u2211\",\n\t\"minus\":    \"\\u2212\",\n\t\"lowast\":   \"\\u2217\",\n\t\"radic\":    \"\\u221A\",\n\t\"prop\":     \"\\u221D\",\n\t\"infin\":    \"\\u221E\",\n\t\"ang\":      \"\\u2220\",\n\t\"and\":      \"\\u2227\",\n\t\"or\":       \"\\u2228\",\n\t\"cap\":      \"\\u2229\",\n\t\"cup\":      \"\\u222A\",\n\t\"int\":      \"\\u222B\",\n\t\"there4\":   \"\\u2234\",\n\t\"sim\":      \"\\u223C\",\n\t\"cong\":     \"\\u2245\",\n\t\"asymp\":    \"\\u2248\",\n\t\"ne\":       \"\\u2260\",\n\t\"equiv\":    \"\\u2261\",\n\t\"le\":       \"\\u2264\",\n\t\"ge\":       \"\\u2265\",\n\t\"sub\":      \"\\u2282\",\n\t\"sup\":      \"\\u2283\",\n\t\"nsub\":     \"\\u2284\",\n\t\"sube\":     \"\\u2286\",\n\t\"supe\":     \"\\u2287\",\n\t\"oplus\":    \"\\u2295\",\n\t\"otimes\":   \"\\u2297\",\n\t\"perp\":     \"\\u22A5\",\n\t\"sdot\":     \"\\u22C5\",\n\t\"lceil\":    \"\\u2308\",\n\t\"rceil\":    \"\\u2309\",\n\t\"lfloor\":   \"\\u230A\",\n\t\"rfloor\":   \"\\u230B\",\n\t\"lang\":     \"\\u2329\",\n\t\"rang\":     \"\\u232A\",\n\t\"loz\":      \"\\u25CA\",\n\t\"spades\":   \"\\u2660\",\n\t\"clubs\":    \"\\u2663\",\n\t\"hearts\":   \"\\u2665\",\n\t\"diams\":    \"\\u2666\",\n\t\"quot\":     \"\\u0022\",\n\t\"amp\":      \"\\u0026\",\n\t\"lt\":       \"\\u003C\",\n\t\"gt\":       \"\\u003E\",\n\t\"OElig\":    \"\\u0152\",\n\t\"oelig\":    \"\\u0153\",\n\t\"Scaron\":   \"\\u0160\",\n\t\"scaron\":   \"\\u0161\",\n\t\"Yuml\":     \"\\u0178\",\n\t\"circ\":     \"\\u02C6\",\n\t\"tilde\":    \"\\u02DC\",\n\t\"ensp\":     \"\\u2002\",\n\t\"emsp\":     \"\\u2003\",\n\t\"thinsp\":   \"\\u2009\",\n\t\"zwnj\":     \"\\u200C\",\n\t\"zwj\":      \"\\u200D\",\n\t\"lrm\":      \"\\u200E\",\n\t\"rlm\":      \"\\u200F\",\n\t\"ndash\":    \"\\u2013\",\n\t\"mdash\":    \"\\u2014\",\n\t\"lsquo\":    \"\\u2018\",\n\t\"rsquo\":    \"\\u2019\",\n\t\"sbquo\":    \"\\u201A\",\n\t\"ldquo\":    \"\\u201C\",\n\t\"rdquo\":    \"\\u201D\",\n\t\"bdquo\":    \"\\u201E\",\n\t\"dagger\":   \"\\u2020\",\n\t\"Dagger\":   \"\\u2021\",\n\t\"permil\":   \"\\u2030\",\n\t\"lsaquo\":   \"\\u2039\",\n\t\"rsaquo\":   \"\\u203A\",\n\t\"euro\":     \"\\u20AC\",\n}\n\n// HTMLAutoClose is the set of HTML elements that\n// should be considered to close automatically.\nvar HTMLAutoClose = htmlAutoClose\n\nvar htmlAutoClose = []string{\n\t/*\n\t\thget http://www.w3.org/TR/html4/loose.dtd |\n\t\t9 sed -n 's/<!ELEMENT ([^ ]*) +- O EMPTY.+/\t\"\\1\",/p' | tr A-Z a-z\n\t*/\n\t\"basefont\",\n\t\"br\",\n\t\"area\",\n\t\"link\",\n\t\"img\",\n\t\"param\",\n\t\"hr\",\n\t\"input\",\n\t\"col\",\n\t\"frame\",\n\t\"isindex\",\n\t\"base\",\n\t\"meta\",\n}\n\nvar (\n\tesc_quot = []byte(\"&#34;\") // shorter than \"&quot;\"\n\tesc_apos = []byte(\"&#39;\") // shorter than \"&apos;\"\n\tesc_amp  = []byte(\"&amp;\")\n\tesc_lt   = []byte(\"&lt;\")\n\tesc_gt   = []byte(\"&gt;\")\n\tesc_tab  = []byte(\"&#x9;\")\n\tesc_nl   = []byte(\"&#xA;\")\n\tesc_cr   = []byte(\"&#xD;\")\n\tesc_fffd = []byte(\"\\uFFFD\") // Unicode replacement character\n)\n\n// EscapeText writes to w the properly escaped XML equivalent\n// of the plain text data s.\nfunc EscapeText(w io.Writer, s []byte) error {\n\treturn escapeText(w, s, true)\n}\n\n// escapeText writes to w the properly escaped XML equivalent\n// of the plain text data s. If escapeNewline is true, newline\n// characters will be escaped.\nfunc escapeText(w io.Writer, s []byte, escapeNewline bool) error {\n\tvar esc []byte\n\tlast := 0\n\tfor i := 0; i < len(s); {\n\t\tr, width := utf8.DecodeRune(s[i:])\n\t\ti += width\n\t\tswitch r {\n\t\tcase '\"':\n\t\t\tesc = esc_quot\n\t\tcase '\\'':\n\t\t\tesc = esc_apos\n\t\tcase '&':\n\t\t\tesc = esc_amp\n\t\tcase '<':\n\t\t\tesc = esc_lt\n\t\tcase '>':\n\t\t\tesc = esc_gt\n\t\tcase '\\t':\n\t\t\tesc = esc_tab\n\t\tcase '\\n':\n\t\t\tif !escapeNewline {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tesc = esc_nl\n\t\tcase '\\r':\n\t\t\tesc = esc_cr\n\t\tdefault:\n\t\t\tif !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {\n\t\t\t\tesc = esc_fffd\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err := w.Write(s[last : i-width]); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := w.Write(esc); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlast = i\n\t}\n\tif _, err := w.Write(s[last:]); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// EscapeString writes to p the properly escaped XML equivalent\n// of the plain text data s.\nfunc (p *printer) EscapeString(s string) {\n\tvar esc []byte\n\tlast := 0\n\tfor i := 0; i < len(s); {\n\t\tr, width := utf8.DecodeRuneInString(s[i:])\n\t\ti += width\n\t\tswitch r {\n\t\tcase '\"':\n\t\t\tesc = esc_quot\n\t\tcase '\\'':\n\t\t\tesc = esc_apos\n\t\tcase '&':\n\t\t\tesc = esc_amp\n\t\tcase '<':\n\t\t\tesc = esc_lt\n\t\tcase '>':\n\t\t\tesc = esc_gt\n\t\tcase '\\t':\n\t\t\tesc = esc_tab\n\t\tcase '\\n':\n\t\t\tesc = esc_nl\n\t\tcase '\\r':\n\t\t\tesc = esc_cr\n\t\tdefault:\n\t\t\tif !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {\n\t\t\t\tesc = esc_fffd\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tp.WriteString(s[last : i-width])\n\t\tp.Write(esc)\n\t\tlast = i\n\t}\n\tp.WriteString(s[last:])\n}\n\n// Escape is like EscapeText but omits the error return value.\n// It is provided for backwards compatibility with Go 1.0.\n// Code targeting Go 1.1 or later should use EscapeText.\nfunc Escape(w io.Writer, s []byte) {\n\tEscapeText(w, s)\n}\n\n// procInst parses the `param=\"...\"` or `param='...'`\n// value out of the provided string, returning \"\" if not found.\nfunc procInst(param, s string) string {\n\t// TODO: this parsing is somewhat lame and not exact.\n\t// It works for all actual cases, though.\n\tparam = param + \"=\"\n\tidx := strings.Index(s, param)\n\tif idx == -1 {\n\t\treturn \"\"\n\t}\n\tv := s[idx+len(param):]\n\tif v == \"\" {\n\t\treturn \"\"\n\t}\n\tif v[0] != '\\'' && v[0] != '\"' {\n\t\treturn \"\"\n\t}\n\tidx = strings.IndexRune(v[1:], rune(v[0]))\n\tif idx == -1 {\n\t\treturn \"\"\n\t}\n\treturn v[1 : idx+1]\n}\n"
  },
  {
    "path": "server/webdav/internal/xml/xml_test.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage xml\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nconst testInput = `\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<body xmlns:foo=\"ns1\" xmlns=\"ns2\" xmlns:tag=\"ns3\" ` +\n\t\"\\r\\n\\t\" + `  >\n  <hello lang=\"en\">World &lt;&gt;&apos;&quot; &#x767d;&#40300;翔</hello>\n  <query>&何; &is-it;</query>\n  <goodbye />\n  <outer foo:attr=\"value\" xmlns:tag=\"ns4\">\n    <inner/>\n  </outer>\n  <tag:name>\n    <![CDATA[Some text here.]]>\n  </tag:name>\n</body><!-- missing final newline -->`\n\nvar testEntity = map[string]string{\"何\": \"What\", \"is-it\": \"is it?\"}\n\nvar rawTokens = []Token{\n\tCharData(\"\\n\"),\n\tProcInst{\"xml\", []byte(`version=\"1.0\" encoding=\"UTF-8\"`)},\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"`),\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"body\"}, []Attr{{Name{\"xmlns\", \"foo\"}, \"ns1\"}, {Name{\"\", \"xmlns\"}, \"ns2\"}, {Name{\"xmlns\", \"tag\"}, \"ns3\"}}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"\", \"hello\"}, []Attr{{Name{\"\", \"lang\"}, \"en\"}}},\n\tCharData(\"World <>'\\\" 白鵬翔\"),\n\tEndElement{Name{\"\", \"hello\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"\", \"query\"}, []Attr{}},\n\tCharData(\"What is it?\"),\n\tEndElement{Name{\"\", \"query\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"\", \"goodbye\"}, []Attr{}},\n\tEndElement{Name{\"\", \"goodbye\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"\", \"outer\"}, []Attr{{Name{\"foo\", \"attr\"}, \"value\"}, {Name{\"xmlns\", \"tag\"}, \"ns4\"}}},\n\tCharData(\"\\n    \"),\n\tStartElement{Name{\"\", \"inner\"}, []Attr{}},\n\tEndElement{Name{\"\", \"inner\"}},\n\tCharData(\"\\n  \"),\n\tEndElement{Name{\"\", \"outer\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"tag\", \"name\"}, []Attr{}},\n\tCharData(\"\\n    \"),\n\tCharData(\"Some text here.\"),\n\tCharData(\"\\n  \"),\n\tEndElement{Name{\"tag\", \"name\"}},\n\tCharData(\"\\n\"),\n\tEndElement{Name{\"\", \"body\"}},\n\tComment(\" missing final newline \"),\n}\n\nvar cookedTokens = []Token{\n\tCharData(\"\\n\"),\n\tProcInst{\"xml\", []byte(`version=\"1.0\" encoding=\"UTF-8\"`)},\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"`),\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"ns2\", \"body\"}, []Attr{{Name{\"xmlns\", \"foo\"}, \"ns1\"}, {Name{\"\", \"xmlns\"}, \"ns2\"}, {Name{\"xmlns\", \"tag\"}, \"ns3\"}}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"ns2\", \"hello\"}, []Attr{{Name{\"\", \"lang\"}, \"en\"}}},\n\tCharData(\"World <>'\\\" 白鵬翔\"),\n\tEndElement{Name{\"ns2\", \"hello\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"ns2\", \"query\"}, []Attr{}},\n\tCharData(\"What is it?\"),\n\tEndElement{Name{\"ns2\", \"query\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"ns2\", \"goodbye\"}, []Attr{}},\n\tEndElement{Name{\"ns2\", \"goodbye\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"ns2\", \"outer\"}, []Attr{{Name{\"ns1\", \"attr\"}, \"value\"}, {Name{\"xmlns\", \"tag\"}, \"ns4\"}}},\n\tCharData(\"\\n    \"),\n\tStartElement{Name{\"ns2\", \"inner\"}, []Attr{}},\n\tEndElement{Name{\"ns2\", \"inner\"}},\n\tCharData(\"\\n  \"),\n\tEndElement{Name{\"ns2\", \"outer\"}},\n\tCharData(\"\\n  \"),\n\tStartElement{Name{\"ns3\", \"name\"}, []Attr{}},\n\tCharData(\"\\n    \"),\n\tCharData(\"Some text here.\"),\n\tCharData(\"\\n  \"),\n\tEndElement{Name{\"ns3\", \"name\"}},\n\tCharData(\"\\n\"),\n\tEndElement{Name{\"ns2\", \"body\"}},\n\tComment(\" missing final newline \"),\n}\n\nconst testInputAltEncoding = `\n<?xml version=\"1.0\" encoding=\"x-testing-uppercase\"?>\n<TAG>VALUE</TAG>`\n\nvar rawTokensAltEncoding = []Token{\n\tCharData(\"\\n\"),\n\tProcInst{\"xml\", []byte(`version=\"1.0\" encoding=\"x-testing-uppercase\"`)},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"value\"),\n\tEndElement{Name{\"\", \"tag\"}},\n}\n\nvar xmlInput = []string{\n\t// unexpected EOF cases\n\t\"<\",\n\t\"<t\",\n\t\"<t \",\n\t\"<t/\",\n\t\"<!\",\n\t\"<!-\",\n\t\"<!--\",\n\t\"<!--c-\",\n\t\"<!--c--\",\n\t\"<!d\",\n\t\"<t></\",\n\t\"<t></t\",\n\t\"<?\",\n\t\"<?p\",\n\t\"<t a\",\n\t\"<t a=\",\n\t\"<t a='\",\n\t\"<t a=''\",\n\t\"<t/><![\",\n\t\"<t/><![C\",\n\t\"<t/><![CDATA[d\",\n\t\"<t/><![CDATA[d]\",\n\t\"<t/><![CDATA[d]]\",\n\n\t// other Syntax errors\n\t\"<>\",\n\t\"<t/a\",\n\t\"<0 />\",\n\t\"<?0 >\",\n\t//\t\"<!0 >\",\t// let the Token() caller handle\n\t\"</0>\",\n\t\"<t 0=''>\",\n\t\"<t a='&'>\",\n\t\"<t a='<'>\",\n\t\"<t>&nbspc;</t>\",\n\t\"<t a>\",\n\t\"<t a=>\",\n\t\"<t a=v>\",\n\t//\t\"<![CDATA[d]]>\",\t// let the Token() caller handle\n\t\"<t></e>\",\n\t\"<t></>\",\n\t\"<t></t!\",\n\t\"<t>cdata]]></t>\",\n}\n\nfunc TestRawToken(t *testing.T) {\n\td := NewDecoder(strings.NewReader(testInput))\n\td.Entity = testEntity\n\ttestRawToken(t, d, testInput, rawTokens)\n}\n\nconst nonStrictInput = `\n<tag>non&entity</tag>\n<tag>&unknown;entity</tag>\n<tag>&#123</tag>\n<tag>&#zzz;</tag>\n<tag>&なまえ3;</tag>\n<tag>&lt-gt;</tag>\n<tag>&;</tag>\n<tag>&0a;</tag>\n`\n\nvar nonStringEntity = map[string]string{\"\": \"oops!\", \"0a\": \"oops!\"}\n\nvar nonStrictTokens = []Token{\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"non&entity\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&unknown;entity\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&#123\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&#zzz;\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&なまえ3;\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&lt-gt;\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&;\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n\tCharData(\"&0a;\"),\n\tEndElement{Name{\"\", \"tag\"}},\n\tCharData(\"\\n\"),\n}\n\nfunc TestNonStrictRawToken(t *testing.T) {\n\td := NewDecoder(strings.NewReader(nonStrictInput))\n\td.Strict = false\n\ttestRawToken(t, d, nonStrictInput, nonStrictTokens)\n}\n\ntype downCaser struct {\n\tt *testing.T\n\tr io.ByteReader\n}\n\nfunc (d *downCaser) ReadByte() (c byte, err error) {\n\tc, err = d.r.ReadByte()\n\tif c >= 'A' && c <= 'Z' {\n\t\tc += 'a' - 'A'\n\t}\n\treturn\n}\n\nfunc (d *downCaser) Read(p []byte) (int, error) {\n\td.t.Fatalf(\"unexpected Read call on downCaser reader\")\n\tpanic(\"unreachable\")\n}\n\nfunc TestRawTokenAltEncoding(t *testing.T) {\n\td := NewDecoder(strings.NewReader(testInputAltEncoding))\n\td.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {\n\t\tif charset != \"x-testing-uppercase\" {\n\t\t\tt.Fatalf(\"unexpected charset %q\", charset)\n\t\t}\n\t\treturn &downCaser{t, input.(io.ByteReader)}, nil\n\t}\n\ttestRawToken(t, d, testInputAltEncoding, rawTokensAltEncoding)\n}\n\nfunc TestRawTokenAltEncodingNoConverter(t *testing.T) {\n\td := NewDecoder(strings.NewReader(testInputAltEncoding))\n\ttoken, err := d.RawToken()\n\tif token == nil {\n\t\tt.Fatalf(\"expected a token on first RawToken call\")\n\t}\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttoken, err = d.RawToken()\n\tif token != nil {\n\t\tt.Errorf(\"expected a nil token; got %#v\", token)\n\t}\n\tif err == nil {\n\t\tt.Fatalf(\"expected an error on second RawToken call\")\n\t}\n\tconst encoding = \"x-testing-uppercase\"\n\tif !strings.Contains(err.Error(), encoding) {\n\t\tt.Errorf(\"expected error to contain %q; got error: %v\",\n\t\t\tencoding, err)\n\t}\n}\n\nfunc testRawToken(t *testing.T, d *Decoder, raw string, rawTokens []Token) {\n\tlastEnd := int64(0)\n\tfor i, want := range rawTokens {\n\t\tstart := d.InputOffset()\n\t\thave, err := d.RawToken()\n\t\tend := d.InputOffset()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"token %d: unexpected error: %s\", i, err)\n\t\t}\n\t\tif !reflect.DeepEqual(have, want) {\n\t\t\tvar shave, swant string\n\t\t\tif _, ok := have.(CharData); ok {\n\t\t\t\tshave = fmt.Sprintf(\"CharData(%q)\", have)\n\t\t\t} else {\n\t\t\t\tshave = fmt.Sprintf(\"%#v\", have)\n\t\t\t}\n\t\t\tif _, ok := want.(CharData); ok {\n\t\t\t\tswant = fmt.Sprintf(\"CharData(%q)\", want)\n\t\t\t} else {\n\t\t\t\tswant = fmt.Sprintf(\"%#v\", want)\n\t\t\t}\n\t\t\tt.Errorf(\"token %d = %s, want %s\", i, shave, swant)\n\t\t}\n\n\t\t// Check that InputOffset returned actual token.\n\t\tswitch {\n\t\tcase start < lastEnd:\n\t\t\tt.Errorf(\"token %d: position [%d,%d) for %T is before previous token\", i, start, end, have)\n\t\tcase start >= end:\n\t\t\t// Special case: EndElement can be synthesized.\n\t\t\tif start == end && end == lastEnd {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tt.Errorf(\"token %d: position [%d,%d) for %T is empty\", i, start, end, have)\n\t\tcase end > int64(len(raw)):\n\t\t\tt.Errorf(\"token %d: position [%d,%d) for %T extends beyond input\", i, start, end, have)\n\t\tdefault:\n\t\t\ttext := raw[start:end]\n\t\t\tif strings.ContainsAny(text, \"<>\") && (!strings.HasPrefix(text, \"<\") || !strings.HasSuffix(text, \">\")) {\n\t\t\t\tt.Errorf(\"token %d: misaligned raw token %#q for %T\", i, text, have)\n\t\t\t}\n\t\t}\n\t\tlastEnd = end\n\t}\n}\n\n// Ensure that directives (specifically !DOCTYPE) include the complete\n// text of any nested directives, noting that < and > do not change\n// nesting depth if they are in single or double quotes.\n\nvar nestedDirectivesInput = `\n<!DOCTYPE [<!ENTITY rdf \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">]>\n<!DOCTYPE [<!ENTITY xlt \">\">]>\n<!DOCTYPE [<!ENTITY xlt \"<\">]>\n<!DOCTYPE [<!ENTITY xlt '>'>]>\n<!DOCTYPE [<!ENTITY xlt '<'>]>\n<!DOCTYPE [<!ENTITY xlt '\">'>]>\n<!DOCTYPE [<!ENTITY xlt \"'<\">]>\n`\n\nvar nestedDirectivesTokens = []Token{\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY rdf \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt \">\">]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt \"<\">]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt '>'>]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt '<'>]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt '\">'>]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY xlt \"'<\">]`),\n\tCharData(\"\\n\"),\n}\n\nfunc TestNestedDirectives(t *testing.T) {\n\td := NewDecoder(strings.NewReader(nestedDirectivesInput))\n\n\tfor i, want := range nestedDirectivesTokens {\n\t\thave, err := d.Token()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"token %d: unexpected error: %s\", i, err)\n\t\t}\n\t\tif !reflect.DeepEqual(have, want) {\n\t\t\tt.Errorf(\"token %d = %#v want %#v\", i, have, want)\n\t\t}\n\t}\n}\n\nfunc TestToken(t *testing.T) {\n\td := NewDecoder(strings.NewReader(testInput))\n\td.Entity = testEntity\n\n\tfor i, want := range cookedTokens {\n\t\thave, err := d.Token()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"token %d: unexpected error: %s\", i, err)\n\t\t}\n\t\tif !reflect.DeepEqual(have, want) {\n\t\t\tt.Errorf(\"token %d = %#v want %#v\", i, have, want)\n\t\t}\n\t}\n}\n\nfunc TestSyntax(t *testing.T) {\n\tfor i := range xmlInput {\n\t\td := NewDecoder(strings.NewReader(xmlInput[i]))\n\t\tvar err error\n\t\tfor _, err = d.Token(); err == nil; _, err = d.Token() {\n\t\t}\n\t\tif _, ok := err.(*SyntaxError); !ok {\n\t\t\tt.Fatalf(`xmlInput \"%s\": expected SyntaxError not received`, xmlInput[i])\n\t\t}\n\t}\n}\n\ntype allScalars struct {\n\tTrue1     bool\n\tTrue2     bool\n\tFalse1    bool\n\tFalse2    bool\n\tInt       int\n\tInt8      int8\n\tInt16     int16\n\tInt32     int32\n\tInt64     int64\n\tUint      int\n\tUint8     uint8\n\tUint16    uint16\n\tUint32    uint32\n\tUint64    uint64\n\tUintptr   uintptr\n\tFloat32   float32\n\tFloat64   float64\n\tString    string\n\tPtrString *string\n}\n\nvar all = allScalars{\n\tTrue1:     true,\n\tTrue2:     true,\n\tFalse1:    false,\n\tFalse2:    false,\n\tInt:       1,\n\tInt8:      -2,\n\tInt16:     3,\n\tInt32:     -4,\n\tInt64:     5,\n\tUint:      6,\n\tUint8:     7,\n\tUint16:    8,\n\tUint32:    9,\n\tUint64:    10,\n\tUintptr:   11,\n\tFloat32:   13.0,\n\tFloat64:   14.0,\n\tString:    \"15\",\n\tPtrString: &sixteen,\n}\n\nvar sixteen = \"16\"\n\nconst testScalarsInput = `<allscalars>\n\t<True1>true</True1>\n\t<True2>1</True2>\n\t<False1>false</False1>\n\t<False2>0</False2>\n\t<Int>1</Int>\n\t<Int8>-2</Int8>\n\t<Int16>3</Int16>\n\t<Int32>-4</Int32>\n\t<Int64>5</Int64>\n\t<Uint>6</Uint>\n\t<Uint8>7</Uint8>\n\t<Uint16>8</Uint16>\n\t<Uint32>9</Uint32>\n\t<Uint64>10</Uint64>\n\t<Uintptr>11</Uintptr>\n\t<Float>12.0</Float>\n\t<Float32>13.0</Float32>\n\t<Float64>14.0</Float64>\n\t<String>15</String>\n\t<PtrString>16</PtrString>\n</allscalars>`\n\nfunc TestAllScalars(t *testing.T) {\n\tvar a allScalars\n\terr := Unmarshal([]byte(testScalarsInput), &a)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !reflect.DeepEqual(a, all) {\n\t\tt.Errorf(\"have %+v want %+v\", a, all)\n\t}\n}\n\ntype item struct {\n\tField_a string\n}\n\nfunc TestIssue569(t *testing.T) {\n\tdata := `<item><Field_a>abcd</Field_a></item>`\n\tvar i item\n\terr := Unmarshal([]byte(data), &i)\n\n\tif err != nil || i.Field_a != \"abcd\" {\n\t\tt.Fatal(\"Expecting abcd\")\n\t}\n}\n\nfunc TestUnquotedAttrs(t *testing.T) {\n\tdata := \"<tag attr=azAZ09:-_\\t>\"\n\td := NewDecoder(strings.NewReader(data))\n\td.Strict = false\n\ttoken, err := d.Token()\n\tif _, ok := err.(*SyntaxError); ok {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n\tif token.(StartElement).Name.Local != \"tag\" {\n\t\tt.Errorf(\"Unexpected tag name: %v\", token.(StartElement).Name.Local)\n\t}\n\tattr := token.(StartElement).Attr[0]\n\tif attr.Value != \"azAZ09:-_\" {\n\t\tt.Errorf(\"Unexpected attribute value: %v\", attr.Value)\n\t}\n\tif attr.Name.Local != \"attr\" {\n\t\tt.Errorf(\"Unexpected attribute name: %v\", attr.Name.Local)\n\t}\n}\n\nfunc TestValuelessAttrs(t *testing.T) {\n\ttests := [][3]string{\n\t\t{\"<p nowrap>\", \"p\", \"nowrap\"},\n\t\t{\"<p nowrap >\", \"p\", \"nowrap\"},\n\t\t{\"<input checked/>\", \"input\", \"checked\"},\n\t\t{\"<input checked />\", \"input\", \"checked\"},\n\t}\n\tfor _, test := range tests {\n\t\td := NewDecoder(strings.NewReader(test[0]))\n\t\td.Strict = false\n\t\ttoken, err := d.Token()\n\t\tif _, ok := err.(*SyntaxError); ok {\n\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t}\n\t\tif token.(StartElement).Name.Local != test[1] {\n\t\t\tt.Errorf(\"Unexpected tag name: %v\", token.(StartElement).Name.Local)\n\t\t}\n\t\tattr := token.(StartElement).Attr[0]\n\t\tif attr.Value != test[2] {\n\t\t\tt.Errorf(\"Unexpected attribute value: %v\", attr.Value)\n\t\t}\n\t\tif attr.Name.Local != test[2] {\n\t\t\tt.Errorf(\"Unexpected attribute name: %v\", attr.Name.Local)\n\t\t}\n\t}\n}\n\nfunc TestCopyTokenCharData(t *testing.T) {\n\tdata := []byte(\"same data\")\n\tvar tok1 Token = CharData(data)\n\ttok2 := CopyToken(tok1)\n\tif !reflect.DeepEqual(tok1, tok2) {\n\t\tt.Error(\"CopyToken(CharData) != CharData\")\n\t}\n\tdata[1] = 'o'\n\tif reflect.DeepEqual(tok1, tok2) {\n\t\tt.Error(\"CopyToken(CharData) uses same buffer.\")\n\t}\n}\n\nfunc TestCopyTokenStartElement(t *testing.T) {\n\telt := StartElement{Name{\"\", \"hello\"}, []Attr{{Name{\"\", \"lang\"}, \"en\"}}}\n\tvar tok1 Token = elt\n\ttok2 := CopyToken(tok1)\n\tif tok1.(StartElement).Attr[0].Value != \"en\" {\n\t\tt.Error(\"CopyToken overwrote Attr[0]\")\n\t}\n\tif !reflect.DeepEqual(tok1, tok2) {\n\t\tt.Error(\"CopyToken(StartElement) != StartElement\")\n\t}\n\ttok1.(StartElement).Attr[0] = Attr{Name{\"\", \"lang\"}, \"de\"}\n\tif reflect.DeepEqual(tok1, tok2) {\n\t\tt.Error(\"CopyToken(CharData) uses same buffer.\")\n\t}\n}\n\nfunc TestSyntaxErrorLineNum(t *testing.T) {\n\ttestInput := \"<P>Foo<P>\\n\\n<P>Bar</>\\n\"\n\td := NewDecoder(strings.NewReader(testInput))\n\tvar err error\n\tfor _, err = d.Token(); err == nil; _, err = d.Token() {\n\t}\n\tsynerr, ok := err.(*SyntaxError)\n\tif !ok {\n\t\tt.Error(\"Expected SyntaxError.\")\n\t}\n\tif synerr.Line != 3 {\n\t\tt.Error(\"SyntaxError didn't have correct line number.\")\n\t}\n}\n\nfunc TestTrailingRawToken(t *testing.T) {\n\tinput := `<FOO></FOO>  `\n\td := NewDecoder(strings.NewReader(input))\n\tvar err error\n\tfor _, err = d.RawToken(); err == nil; _, err = d.RawToken() {\n\t}\n\tif err != io.EOF {\n\t\tt.Fatalf(\"d.RawToken() = _, %v, want _, io.EOF\", err)\n\t}\n}\n\nfunc TestTrailingToken(t *testing.T) {\n\tinput := `<FOO></FOO>  `\n\td := NewDecoder(strings.NewReader(input))\n\tvar err error\n\tfor _, err = d.Token(); err == nil; _, err = d.Token() {\n\t}\n\tif err != io.EOF {\n\t\tt.Fatalf(\"d.Token() = _, %v, want _, io.EOF\", err)\n\t}\n}\n\nfunc TestEntityInsideCDATA(t *testing.T) {\n\tinput := `<test><![CDATA[ &val=foo ]]></test>`\n\td := NewDecoder(strings.NewReader(input))\n\tvar err error\n\tfor _, err = d.Token(); err == nil; _, err = d.Token() {\n\t}\n\tif err != io.EOF {\n\t\tt.Fatalf(\"d.Token() = _, %v, want _, io.EOF\", err)\n\t}\n}\n\nvar characterTests = []struct {\n\tin  string\n\terr string\n}{\n\t{\"\\x12<doc/>\", \"illegal character code U+0012\"},\n\t{\"<?xml version=\\\"1.0\\\"?>\\x0b<doc/>\", \"illegal character code U+000B\"},\n\t{\"\\xef\\xbf\\xbe<doc/>\", \"illegal character code U+FFFE\"},\n\t{\"<?xml version=\\\"1.0\\\"?><doc>\\r\\n<hiya/>\\x07<toots/></doc>\", \"illegal character code U+0007\"},\n\t{\"<?xml version=\\\"1.0\\\"?><doc \\x12='value'>what's up</doc>\", \"expected attribute name in element\"},\n\t{\"<doc>&abc\\x01;</doc>\", \"invalid character entity &abc (no semicolon)\"},\n\t{\"<doc>&\\x01;</doc>\", \"invalid character entity & (no semicolon)\"},\n\t{\"<doc>&\\xef\\xbf\\xbe;</doc>\", \"invalid character entity &\\uFFFE;\"},\n\t{\"<doc>&hello;</doc>\", \"invalid character entity &hello;\"},\n}\n\nfunc TestDisallowedCharacters(t *testing.T) {\n\n\tfor i, tt := range characterTests {\n\t\td := NewDecoder(strings.NewReader(tt.in))\n\t\tvar err error\n\n\t\tfor err == nil {\n\t\t\t_, err = d.Token()\n\t\t}\n\t\tsynerr, ok := err.(*SyntaxError)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"input %d d.Token() = _, %v, want _, *SyntaxError\", i, err)\n\t\t}\n\t\tif synerr.Msg != tt.err {\n\t\t\tt.Fatalf(\"input %d synerr.Msg wrong: want %q, got %q\", i, tt.err, synerr.Msg)\n\t\t}\n\t}\n}\n\ntype procInstEncodingTest struct {\n\texpect, got string\n}\n\nvar procInstTests = []struct {\n\tinput  string\n\texpect [2]string\n}{\n\t{`version=\"1.0\" encoding=\"utf-8\"`, [2]string{\"1.0\", \"utf-8\"}},\n\t{`version=\"1.0\" encoding='utf-8'`, [2]string{\"1.0\", \"utf-8\"}},\n\t{`version=\"1.0\" encoding='utf-8' `, [2]string{\"1.0\", \"utf-8\"}},\n\t{`version=\"1.0\" encoding=utf-8`, [2]string{\"1.0\", \"\"}},\n\t{`encoding=\"FOO\" `, [2]string{\"\", \"FOO\"}},\n}\n\nfunc TestProcInstEncoding(t *testing.T) {\n\tfor _, test := range procInstTests {\n\t\tif got := procInst(\"version\", test.input); got != test.expect[0] {\n\t\t\tt.Errorf(\"procInst(version, %q) = %q; want %q\", test.input, got, test.expect[0])\n\t\t}\n\t\tif got := procInst(\"encoding\", test.input); got != test.expect[1] {\n\t\t\tt.Errorf(\"procInst(encoding, %q) = %q; want %q\", test.input, got, test.expect[1])\n\t\t}\n\t}\n}\n\n// Ensure that directives with comments include the complete\n// text of any nested directives.\n\nvar directivesWithCommentsInput = `\n<!DOCTYPE [<!-- a comment --><!ENTITY rdf \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">]>\n<!DOCTYPE [<!ENTITY go \"Golang\"><!-- a comment-->]>\n<!DOCTYPE <!-> <!> <!----> <!-->--> <!--->--> [<!ENTITY go \"Golang\"><!-- a comment-->]>\n`\n\nvar directivesWithCommentsTokens = []Token{\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY rdf \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE [<!ENTITY go \"Golang\">]`),\n\tCharData(\"\\n\"),\n\tDirective(`DOCTYPE <!-> <!>    [<!ENTITY go \"Golang\">]`),\n\tCharData(\"\\n\"),\n}\n\nfunc TestDirectivesWithComments(t *testing.T) {\n\td := NewDecoder(strings.NewReader(directivesWithCommentsInput))\n\n\tfor i, want := range directivesWithCommentsTokens {\n\t\thave, err := d.Token()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"token %d: unexpected error: %s\", i, err)\n\t\t}\n\t\tif !reflect.DeepEqual(have, want) {\n\t\t\tt.Errorf(\"token %d = %#v want %#v\", i, have, want)\n\t\t}\n\t}\n}\n\n// Writer whose Write method always returns an error.\ntype errWriter struct{}\n\nfunc (errWriter) Write(p []byte) (n int, err error) { return 0, fmt.Errorf(\"unwritable\") }\n\nfunc TestEscapeTextIOErrors(t *testing.T) {\n\texpectErr := \"unwritable\"\n\terr := EscapeText(errWriter{}, []byte{'A'})\n\n\tif err == nil || err.Error() != expectErr {\n\t\tt.Errorf(\"have %v, want %v\", err, expectErr)\n\t}\n}\n\nfunc TestEscapeTextInvalidChar(t *testing.T) {\n\tinput := []byte(\"A \\x00 terminated string.\")\n\texpected := \"A \\uFFFD terminated string.\"\n\n\tbuff := new(bytes.Buffer)\n\tif err := EscapeText(buff, input); err != nil {\n\t\tt.Fatalf(\"have %v, want nil\", err)\n\t}\n\ttext := buff.String()\n\n\tif text != expected {\n\t\tt.Errorf(\"have %v, want %v\", text, expected)\n\t}\n}\n\nfunc TestIssue5880(t *testing.T) {\n\ttype T []byte\n\tdata, err := Marshal(T{192, 168, 0, 1})\n\tif err != nil {\n\t\tt.Errorf(\"Marshal error: %v\", err)\n\t}\n\tif !utf8.Valid(data) {\n\t\tt.Errorf(\"Marshal generated invalid UTF-8: %x\", data)\n\t}\n}\n"
  },
  {
    "path": "server/webdav/litmus_test_server.go",
    "content": "// Copyright 2015 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build ignore\n// +build ignore\n\n/*\nThis program is a server for the WebDAV 'litmus' compliance test at\nhttp://www.webdav.org/neon/litmus/\nTo run the test:\n\ngo run litmus_test_server.go\n\nand separately, from the downloaded litmus-xxx directory:\n\nmake URL=http://localhost:9999/ check\n*/\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"golang.org/x/net/webdav\"\n)\n\nvar port = flag.Int(\"port\", 9999, \"server port\")\n\nfunc main() {\n\tflag.Parse()\n\tlog.SetFlags(0)\n\th := &webdav.Handler{\n\t\tFileSystem: webdav.NewMemFS(),\n\t\tLockSystem: webdav.NewMemLS(),\n\t\tLogger: func(r *http.Request, err error) {\n\t\t\tlitmus := r.Header.Get(\"X-Litmus\")\n\t\t\tif len(litmus) > 19 {\n\t\t\t\tlitmus = litmus[:16] + \"...\"\n\t\t\t}\n\n\t\t\tswitch r.Method {\n\t\t\tcase \"COPY\", \"MOVE\":\n\t\t\t\tdst := \"\"\n\t\t\t\tif u, err := url.Parse(r.Header.Get(\"Destination\")); err == nil {\n\t\t\t\t\tdst = u.Path\n\t\t\t\t}\n\t\t\t\to := r.Header.Get(\"Overwrite\")\n\t\t\t\tlog.Printf(\"%-20s%-10s%-30s%-30so=%-2s%v\", litmus, r.Method, r.URL.Path, dst, o, err)\n\t\t\tdefault:\n\t\t\t\tlog.Printf(\"%-20s%-10s%-30s%v\", litmus, r.Method, r.URL.Path, err)\n\t\t\t}\n\t\t},\n\t}\n\n\t// The next line would normally be:\n\t//\thttp.Handle(\"/\", h)\n\t// but we wrap that HTTP handler h to cater for a special case.\n\t//\n\t// The propfind_invalid2 litmus test case expects an empty namespace prefix\n\t// declaration to be an error. The FAQ in the webdav litmus test says:\n\t//\n\t// \"What does the \"propfind_invalid2\" test check for?...\n\t//\n\t// If a request was sent with an XML body which included an empty namespace\n\t// prefix declaration (xmlns:ns1=\"\"), then the server must reject that with\n\t// a \"400 Bad Request\" response, as it is invalid according to the XML\n\t// Namespace specification.\"\n\t//\n\t// On the other hand, the Go standard library's encoding/xml package\n\t// accepts an empty xmlns namespace, as per the discussion at\n\t// https://github.com/golang/go/issues/8068\n\t//\n\t// Empty namespaces seem disallowed in the second (2006) edition of the XML\n\t// standard, but allowed in a later edition. The grammar differs between\n\t// http://www.w3.org/TR/2006/REC-xml-names-20060816/#ns-decl and\n\t// http://www.w3.org/TR/REC-xml-names/#dt-prefix\n\t//\n\t// Thus, we assume that the propfind_invalid2 test is obsolete, and\n\t// hard-code the 400 Bad Request response that the test expects.\n\thttp.Handle(\"/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Header.Get(\"X-Litmus\") == \"props: 3 (propfind_invalid2)\" {\n\t\t\thttp.Error(w, \"400 Bad Request\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t}))\n\n\taddr := fmt.Sprintf(\":%d\", *port)\n\tlog.Printf(\"Serving %v\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, nil))\n}\n"
  },
  {
    "path": "server/webdav/lock.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\nimport (\n\t\"container/heap\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar (\n\t// ErrConfirmationFailed is returned by a LockSystem's Confirm method.\n\tErrConfirmationFailed = errors.New(\"webdav: confirmation failed\")\n\t// ErrForbidden is returned by a LockSystem's Unlock method.\n\tErrForbidden = errors.New(\"webdav: forbidden\")\n\t// ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.\n\tErrLocked = errors.New(\"webdav: locked\")\n\t// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.\n\tErrNoSuchLock = errors.New(\"webdav: no such lock\")\n)\n\n// Condition can match a WebDAV resource, based on a token or ETag.\n// Exactly one of Token and ETag should be non-empty.\ntype Condition struct {\n\tNot   bool\n\tToken string\n\tETag  string\n}\n\n// LockSystem manages access to a collection of named resources. The elements\n// in a lock name are separated by slash ('/', U+002F) characters, regardless\n// of host operating system convention.\ntype LockSystem interface {\n\t// Confirm confirms that the caller can claim all of the locks specified by\n\t// the given conditions, and that holding the union of all of those locks\n\t// gives exclusive access to all of the named resources. Up to two resources\n\t// can be named. Empty names are ignored.\n\t//\n\t// Exactly one of release and err will be non-nil. If release is non-nil,\n\t// all of the requested locks are held until release is called. Calling\n\t// release does not unlock the lock, in the WebDAV UNLOCK sense, but once\n\t// Confirm has confirmed that a lock claim is valid, that lock cannot be\n\t// Confirmed again until it has been released.\n\t//\n\t// If Confirm returns ErrConfirmationFailed then the Handler will continue\n\t// to try any other set of locks presented (a WebDAV HTTP request can\n\t// present more than one set of locks). If it returns any other non-nil\n\t// error, the Handler will write a \"500 Internal Server Error\" HTTP status.\n\tConfirm(now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error)\n\n\t// Create creates a lock with the given depth, duration, owner and root\n\t// (name). The depth will either be negative (meaning infinite) or zero.\n\t//\n\t// If Create returns ErrLocked then the Handler will write a \"423 Locked\"\n\t// HTTP status. If it returns any other non-nil error, the Handler will\n\t// write a \"500 Internal Server Error\" HTTP status.\n\t//\n\t// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for\n\t// when to use each error.\n\t//\n\t// The token returned identifies the created lock. It should be an absolute\n\t// URI as defined by RFC 3986, Section 4.3. In particular, it should not\n\t// contain whitespace.\n\tCreate(now time.Time, details LockDetails) (token string, err error)\n\n\t// Refresh refreshes the lock with the given token.\n\t//\n\t// If Refresh returns ErrLocked then the Handler will write a \"423 Locked\"\n\t// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write\n\t// a \"412 Precondition Failed\" HTTP Status. If it returns any other non-nil\n\t// error, the Handler will write a \"500 Internal Server Error\" HTTP status.\n\t//\n\t// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for\n\t// when to use each error.\n\tRefresh(now time.Time, token string, duration time.Duration) (LockDetails, error)\n\n\t// Unlock unlocks the lock with the given token.\n\t//\n\t// If Unlock returns ErrForbidden then the Handler will write a \"403\n\t// Forbidden\" HTTP Status. If Unlock returns ErrLocked then the Handler\n\t// will write a \"423 Locked\" HTTP status. If Unlock returns ErrNoSuchLock\n\t// then the Handler will write a \"409 Conflict\" HTTP Status. If it returns\n\t// any other non-nil error, the Handler will write a \"500 Internal Server\n\t// Error\" HTTP status.\n\t//\n\t// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for\n\t// when to use each error.\n\tUnlock(now time.Time, token string) error\n}\n\n// LockDetails are a lock's metadata.\ntype LockDetails struct {\n\t// Root is the root resource name being locked. For a zero-depth lock, the\n\t// root is the only resource being locked.\n\tRoot string\n\t// Duration is the lock timeout. A negative duration means infinite.\n\tDuration time.Duration\n\t// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.\n\t//\n\t// TODO: does the \"verbatim\" nature play well with XML namespaces?\n\t// Does the OwnerXML field need to have more structure? See\n\t// https://codereview.appspot.com/175140043/#msg2\n\tOwnerXML string\n\t// ZeroDepth is whether the lock has zero depth. If it does not have zero\n\t// depth, it has infinite depth.\n\tZeroDepth bool\n}\n\n// NewMemLS returns a new in-memory LockSystem.\nfunc NewMemLS() LockSystem {\n\treturn &memLS{\n\t\tbyName:  make(map[string]*memLSNode),\n\t\tbyToken: make(map[string]*memLSNode),\n\t\tgen:     uint64(time.Now().Unix()),\n\t}\n}\n\ntype memLS struct {\n\tmu      sync.Mutex\n\tbyName  map[string]*memLSNode\n\tbyToken map[string]*memLSNode\n\tgen     uint64\n\t// byExpiry only contains those nodes whose LockDetails have a finite\n\t// Duration and are yet to expire.\n\tbyExpiry byExpiry\n}\n\nfunc (m *memLS) nextToken() string {\n\tm.gen++\n\treturn strconv.FormatUint(m.gen, 10)\n}\n\nfunc (m *memLS) collectExpiredNodes(now time.Time) {\n\tfor len(m.byExpiry) > 0 {\n\t\tif now.Before(m.byExpiry[0].expiry) {\n\t\t\tbreak\n\t\t}\n\t\tm.remove(m.byExpiry[0])\n\t}\n}\n\nfunc (m *memLS) Confirm(now time.Time, name0, name1 string, conditions ...Condition) (func(), error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.collectExpiredNodes(now)\n\n\tvar n0, n1 *memLSNode\n\tif name0 != \"\" {\n\t\tif n0 = m.lookup(slashClean(name0), conditions...); n0 == nil {\n\t\t\treturn nil, ErrConfirmationFailed\n\t\t}\n\t}\n\tif name1 != \"\" {\n\t\tif n1 = m.lookup(slashClean(name1), conditions...); n1 == nil {\n\t\t\treturn nil, ErrConfirmationFailed\n\t\t}\n\t}\n\n\t// Don't hold the same node twice.\n\tif n1 == n0 {\n\t\tn1 = nil\n\t}\n\n\tif n0 != nil {\n\t\tm.hold(n0)\n\t}\n\tif n1 != nil {\n\t\tm.hold(n1)\n\t}\n\treturn func() {\n\t\tm.mu.Lock()\n\t\tdefer m.mu.Unlock()\n\t\tif n1 != nil {\n\t\t\tm.unhold(n1)\n\t\t}\n\t\tif n0 != nil {\n\t\t\tm.unhold(n0)\n\t\t}\n\t}, nil\n}\n\n// lookup returns the node n that locks the named resource, provided that n\n// matches at least one of the given conditions and that lock isn't held by\n// another party. Otherwise, it returns nil.\n//\n// n may be a parent of the named resource, if n is an infinite depth lock.\nfunc (m *memLS) lookup(name string, conditions ...Condition) (n *memLSNode) {\n\t// TODO: support Condition.Not and Condition.ETag.\n\tfor _, c := range conditions {\n\t\tn = m.byToken[c.Token]\n\t\tif n == nil || n.held {\n\t\t\tcontinue\n\t\t}\n\t\tif name == n.details.Root {\n\t\t\treturn n\n\t\t}\n\t\tif n.details.ZeroDepth {\n\t\t\tcontinue\n\t\t}\n\t\tif n.details.Root == \"/\" || strings.HasPrefix(name, n.details.Root+\"/\") {\n\t\t\treturn n\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *memLS) hold(n *memLSNode) {\n\tif n.held {\n\t\tpanic(\"webdav: memLS inconsistent held state\")\n\t}\n\tn.held = true\n\tif n.details.Duration >= 0 && n.byExpiryIndex >= 0 {\n\t\theap.Remove(&m.byExpiry, n.byExpiryIndex)\n\t}\n}\n\nfunc (m *memLS) unhold(n *memLSNode) {\n\tif !n.held {\n\t\tpanic(\"webdav: memLS inconsistent held state\")\n\t}\n\tn.held = false\n\tif n.details.Duration >= 0 {\n\t\theap.Push(&m.byExpiry, n)\n\t}\n}\n\nfunc (m *memLS) Create(now time.Time, details LockDetails) (string, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.collectExpiredNodes(now)\n\tdetails.Root = slashClean(details.Root)\n\n\tif !m.canCreate(details.Root, details.ZeroDepth) {\n\t\treturn \"\", ErrLocked\n\t}\n\tn := m.create(details.Root)\n\tn.token = m.nextToken()\n\tm.byToken[n.token] = n\n\tn.details = details\n\tif n.details.Duration >= 0 {\n\t\tn.expiry = now.Add(n.details.Duration)\n\t\theap.Push(&m.byExpiry, n)\n\t}\n\treturn n.token, nil\n}\n\nfunc (m *memLS) Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.collectExpiredNodes(now)\n\n\tn := m.byToken[token]\n\tif n == nil {\n\t\treturn LockDetails{}, ErrNoSuchLock\n\t}\n\tif n.held {\n\t\treturn LockDetails{}, ErrLocked\n\t}\n\tif n.byExpiryIndex >= 0 {\n\t\theap.Remove(&m.byExpiry, n.byExpiryIndex)\n\t}\n\tn.details.Duration = duration\n\tif n.details.Duration >= 0 {\n\t\tn.expiry = now.Add(n.details.Duration)\n\t\theap.Push(&m.byExpiry, n)\n\t}\n\treturn n.details, nil\n}\n\nfunc (m *memLS) Unlock(now time.Time, token string) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.collectExpiredNodes(now)\n\n\tn := m.byToken[token]\n\tif n == nil {\n\t\treturn ErrNoSuchLock\n\t}\n\tif n.held {\n\t\treturn ErrLocked\n\t}\n\tm.remove(n)\n\treturn nil\n}\n\nfunc (m *memLS) canCreate(name string, zeroDepth bool) bool {\n\treturn walkToRoot(name, func(name0 string, first bool) bool {\n\t\tn := m.byName[name0]\n\t\tif n == nil {\n\t\t\treturn true\n\t\t}\n\t\tif first {\n\t\t\tif n.token != \"\" {\n\t\t\t\t// The target node is already locked.\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif !zeroDepth {\n\t\t\t\t// The requested lock depth is infinite, and the fact that n exists\n\t\t\t\t// (n != nil) means that a descendent of the target node is locked.\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else if n.token != \"\" && !n.details.ZeroDepth {\n\t\t\t// An ancestor of the target node is locked with infinite depth.\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n}\n\nfunc (m *memLS) create(name string) (ret *memLSNode) {\n\twalkToRoot(name, func(name0 string, first bool) bool {\n\t\tn := m.byName[name0]\n\t\tif n == nil {\n\t\t\tn = &memLSNode{\n\t\t\t\tdetails: LockDetails{\n\t\t\t\t\tRoot: name0,\n\t\t\t\t},\n\t\t\t\tbyExpiryIndex: -1,\n\t\t\t}\n\t\t\tm.byName[name0] = n\n\t\t}\n\t\tn.refCount++\n\t\tif first {\n\t\t\tret = n\n\t\t}\n\t\treturn true\n\t})\n\treturn ret\n}\n\nfunc (m *memLS) remove(n *memLSNode) {\n\tdelete(m.byToken, n.token)\n\tn.token = \"\"\n\twalkToRoot(n.details.Root, func(name0 string, first bool) bool {\n\t\tx := m.byName[name0]\n\t\tx.refCount--\n\t\tif x.refCount == 0 {\n\t\t\tdelete(m.byName, name0)\n\t\t}\n\t\treturn true\n\t})\n\tif n.byExpiryIndex >= 0 {\n\t\theap.Remove(&m.byExpiry, n.byExpiryIndex)\n\t}\n}\n\nfunc walkToRoot(name string, f func(name0 string, first bool) bool) bool {\n\tfor first := true; ; first = false {\n\t\tif !f(name, first) {\n\t\t\treturn false\n\t\t}\n\t\tif name == \"/\" {\n\t\t\tbreak\n\t\t}\n\t\tname = name[:strings.LastIndex(name, \"/\")]\n\t\tif name == \"\" {\n\t\t\tname = \"/\"\n\t\t}\n\t}\n\treturn true\n}\n\ntype memLSNode struct {\n\t// details are the lock metadata. Even if this node's name is not explicitly locked,\n\t// details.Root will still equal the node's name.\n\tdetails LockDetails\n\t// token is the unique identifier for this node's lock. An empty token means that\n\t// this node is not explicitly locked.\n\ttoken string\n\t// refCount is the number of self-or-descendent nodes that are explicitly locked.\n\trefCount int\n\t// expiry is when this node's lock expires.\n\texpiry time.Time\n\t// byExpiryIndex is the index of this node in memLS.byExpiry. It is -1\n\t// if this node does not expire, or has expired.\n\tbyExpiryIndex int\n\t// held is whether this node's lock is actively held by a Confirm call.\n\theld bool\n}\n\ntype byExpiry []*memLSNode\n\nfunc (b *byExpiry) Len() int {\n\treturn len(*b)\n}\n\nfunc (b *byExpiry) Less(i, j int) bool {\n\treturn (*b)[i].expiry.Before((*b)[j].expiry)\n}\n\nfunc (b *byExpiry) Swap(i, j int) {\n\t(*b)[i], (*b)[j] = (*b)[j], (*b)[i]\n\t(*b)[i].byExpiryIndex = i\n\t(*b)[j].byExpiryIndex = j\n}\n\nfunc (b *byExpiry) Push(x interface{}) {\n\tn := x.(*memLSNode)\n\tn.byExpiryIndex = len(*b)\n\t*b = append(*b, n)\n}\n\nfunc (b *byExpiry) Pop() interface{} {\n\ti := len(*b) - 1\n\tn := (*b)[i]\n\t(*b)[i] = nil\n\tn.byExpiryIndex = -1\n\t*b = (*b)[:i]\n\treturn n\n}\n\nconst infiniteTimeout = -1\n\n// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is\n// empty, an infiniteTimeout is returned.\nfunc parseTimeout(s string) (time.Duration, error) {\n\tif s == \"\" {\n\t\treturn infiniteTimeout, nil\n\t}\n\tif i := strings.IndexByte(s, ','); i >= 0 {\n\t\ts = s[:i]\n\t}\n\ts = strings.TrimSpace(s)\n\tif s == \"Infinite\" {\n\t\treturn infiniteTimeout, nil\n\t}\n\tconst pre = \"Second-\"\n\tif !strings.HasPrefix(s, pre) {\n\t\treturn 0, errInvalidTimeout\n\t}\n\ts = s[len(pre):]\n\tif s == \"\" || s[0] < '0' || '9' < s[0] {\n\t\treturn 0, errInvalidTimeout\n\t}\n\tn, err := strconv.ParseInt(s, 10, 64)\n\tif err != nil || 1<<32-1 < n {\n\t\treturn 0, errInvalidTimeout\n\t}\n\treturn time.Duration(n) * time.Second, nil\n}\n"
  },
  {
    "path": "server/webdav/lock_test.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"path\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestWalkToRoot(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\twant []string\n\t}{{\n\t\t\"/a/b/c/d\",\n\t\t[]string{\n\t\t\t\"/a/b/c/d\",\n\t\t\t\"/a/b/c\",\n\t\t\t\"/a/b\",\n\t\t\t\"/a\",\n\t\t\t\"/\",\n\t\t},\n\t}, {\n\t\t\"/a\",\n\t\t[]string{\n\t\t\t\"/a\",\n\t\t\t\"/\",\n\t\t},\n\t}, {\n\t\t\"/\",\n\t\t[]string{\n\t\t\t\"/\",\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tvar got []string\n\t\tif !walkToRoot(tc.name, func(name0 string, first bool) bool {\n\t\t\tif first != (len(got) == 0) {\n\t\t\t\tt.Errorf(\"name=%q: first=%t but len(got)==%d\", tc.name, first, len(got))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tgot = append(got, name0)\n\t\t\treturn true\n\t\t}) {\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\tt.Errorf(\"name=%q:\\ngot  %q\\nwant %q\", tc.name, got, tc.want)\n\t\t}\n\t}\n}\n\nvar lockTestDurations = []time.Duration{\n\tinfiniteTimeout, // infiniteTimeout means to never expire.\n\t0,               // A zero duration means to expire immediately.\n\t100 * time.Hour, // A very large duration will not expire in these tests.\n}\n\n// lockTestNames are the names of a set of mutually compatible locks. For each\n// name fragment:\n//   - _ means no explicit lock.\n//   - i means an infinite-depth lock,\n//   - z means a zero-depth lock,\nvar lockTestNames = []string{\n\t\"/_/_/_/_/z\",\n\t\"/_/_/i\",\n\t\"/_/z\",\n\t\"/_/z/i\",\n\t\"/_/z/z\",\n\t\"/_/z/_/i\",\n\t\"/_/z/_/z\",\n\t\"/i\",\n\t\"/z\",\n\t\"/z/_/i\",\n\t\"/z/_/z\",\n}\n\nfunc lockTestZeroDepth(name string) bool {\n\tswitch name[len(name)-1] {\n\tcase 'i':\n\t\treturn false\n\tcase 'z':\n\t\treturn true\n\t}\n\tpanic(fmt.Sprintf(\"lock name %q did not end with 'i' or 'z'\", name))\n}\n\nfunc TestMemLSCanCreate(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tm := NewMemLS().(*memLS)\n\n\tfor _, name := range lockTestNames {\n\t\t_, err := m.Create(now, LockDetails{\n\t\t\tRoot:      name,\n\t\t\tDuration:  infiniteTimeout,\n\t\t\tZeroDepth: lockTestZeroDepth(name),\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"creating lock for %q: %v\", name, err)\n\t\t}\n\t}\n\n\twantCanCreate := func(name string, zeroDepth bool) bool {\n\t\tfor _, n := range lockTestNames {\n\t\t\tswitch {\n\t\t\tcase n == name:\n\t\t\t\t// An existing lock has the same name as the proposed lock.\n\t\t\t\treturn false\n\t\t\tcase strings.HasPrefix(n, name):\n\t\t\t\t// An existing lock would be a child of the proposed lock,\n\t\t\t\t// which conflicts if the proposed lock has infinite depth.\n\t\t\t\tif !zeroDepth {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\tcase strings.HasPrefix(name, n):\n\t\t\t\t// An existing lock would be an ancestor of the proposed lock,\n\t\t\t\t// which conflicts if the ancestor has infinite depth.\n\t\t\t\tif n[len(n)-1] == 'i' {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\tvar check func(int, string)\n\tcheck = func(recursion int, name string) {\n\t\tfor _, zeroDepth := range []bool{false, true} {\n\t\t\tgot := m.canCreate(name, zeroDepth)\n\t\t\twant := wantCanCreate(name, zeroDepth)\n\t\t\tif got != want {\n\t\t\t\tt.Errorf(\"canCreate name=%q zeroDepth=%t: got %t, want %t\", name, zeroDepth, got, want)\n\t\t\t}\n\t\t}\n\t\tif recursion == 6 {\n\t\t\treturn\n\t\t}\n\t\tif name != \"/\" {\n\t\t\tname += \"/\"\n\t\t}\n\t\tfor _, c := range \"_iz\" {\n\t\t\tcheck(recursion+1, name+string(c))\n\t\t}\n\t}\n\tcheck(0, \"/\")\n}\n\nfunc TestMemLSLookup(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tm := NewMemLS().(*memLS)\n\n\tbadToken := m.nextToken()\n\tt.Logf(\"badToken=%q\", badToken)\n\n\tfor _, name := range lockTestNames {\n\t\ttoken, err := m.Create(now, LockDetails{\n\t\t\tRoot:      name,\n\t\t\tDuration:  infiniteTimeout,\n\t\t\tZeroDepth: lockTestZeroDepth(name),\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"creating lock for %q: %v\", name, err)\n\t\t}\n\t\tt.Logf(\"%-15q -> node=%p token=%q\", name, m.byName[name], token)\n\t}\n\n\tbaseNames := append([]string{\"/a\", \"/b/c\"}, lockTestNames...)\n\tfor _, baseName := range baseNames {\n\t\tfor _, suffix := range []string{\"\", \"/0\", \"/1/2/3\"} {\n\t\t\tname := baseName + suffix\n\n\t\t\tgoodToken := \"\"\n\t\t\tbase := m.byName[baseName]\n\t\t\tif base != nil && (suffix == \"\" || !lockTestZeroDepth(baseName)) {\n\t\t\t\tgoodToken = base.token\n\t\t\t}\n\n\t\t\tfor _, token := range []string{badToken, goodToken} {\n\t\t\t\tif token == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tgot := m.lookup(name, Condition{Token: token})\n\t\t\t\twant := base\n\t\t\t\tif token == badToken {\n\t\t\t\t\twant = nil\n\t\t\t\t}\n\t\t\t\tif got != want {\n\t\t\t\t\tt.Errorf(\"name=%-20qtoken=%q (bad=%t): got %p, want %p\",\n\t\t\t\t\t\tname, token, token == badToken, got, want)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestMemLSConfirm(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tm := NewMemLS().(*memLS)\n\talice, err := m.Create(now, LockDetails{\n\t\tRoot:      \"/alice\",\n\t\tDuration:  infiniteTimeout,\n\t\tZeroDepth: false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create: %v\", err)\n\t}\n\n\ttweedle, err := m.Create(now, LockDetails{\n\t\tRoot:      \"/tweedle\",\n\t\tDuration:  infiniteTimeout,\n\t\tZeroDepth: false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create: %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Create: inconsistent state: %v\", err)\n\t}\n\n\t// Test a mismatch between name and condition.\n\t_, err = m.Confirm(now, \"/tweedle/dee\", \"\", Condition{Token: alice})\n\tif err != ErrConfirmationFailed {\n\t\tt.Fatalf(\"Confirm (mismatch): got %v, want ErrConfirmationFailed\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Confirm (mismatch): inconsistent state: %v\", err)\n\t}\n\n\t// Test two names (that fall under the same lock) in the one Confirm call.\n\trelease, err := m.Confirm(now, \"/tweedle/dee\", \"/tweedle/dum\", Condition{Token: tweedle})\n\tif err != nil {\n\t\tt.Fatalf(\"Confirm (twins): %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Confirm (twins): inconsistent state: %v\", err)\n\t}\n\trelease()\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"release (twins): inconsistent state: %v\", err)\n\t}\n\n\t// Test the same two names in overlapping Confirm / release calls.\n\treleaseDee, err := m.Confirm(now, \"/tweedle/dee\", \"\", Condition{Token: tweedle})\n\tif err != nil {\n\t\tt.Fatalf(\"Confirm (sequence #0): %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Confirm (sequence #0): inconsistent state: %v\", err)\n\t}\n\n\t_, err = m.Confirm(now, \"/tweedle/dum\", \"\", Condition{Token: tweedle})\n\tif err != ErrConfirmationFailed {\n\t\tt.Fatalf(\"Confirm (sequence #1): got %v, want ErrConfirmationFailed\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Confirm (sequence #1): inconsistent state: %v\", err)\n\t}\n\n\treleaseDee()\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"release (sequence #2): inconsistent state: %v\", err)\n\t}\n\n\treleaseDum, err := m.Confirm(now, \"/tweedle/dum\", \"\", Condition{Token: tweedle})\n\tif err != nil {\n\t\tt.Fatalf(\"Confirm (sequence #3): %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Confirm (sequence #3): inconsistent state: %v\", err)\n\t}\n\n\t// Test that you can't unlock a held lock.\n\terr = m.Unlock(now, tweedle)\n\tif err != ErrLocked {\n\t\tt.Fatalf(\"Unlock (sequence #4): got %v, want ErrLocked\", err)\n\t}\n\n\treleaseDum()\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"release (sequence #5): inconsistent state: %v\", err)\n\t}\n\n\terr = m.Unlock(now, tweedle)\n\tif err != nil {\n\t\tt.Fatalf(\"Unlock (sequence #6): %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Unlock (sequence #6): inconsistent state: %v\", err)\n\t}\n}\n\nfunc TestMemLSNonCanonicalRoot(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tm := NewMemLS().(*memLS)\n\ttoken, err := m.Create(now, LockDetails{\n\t\tRoot:     \"/foo/./bar//\",\n\t\tDuration: 1 * time.Second,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create: %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Create: inconsistent state: %v\", err)\n\t}\n\tif err := m.Unlock(now, token); err != nil {\n\t\tt.Fatalf(\"Unlock: %v\", err)\n\t}\n\tif err := m.consistent(); err != nil {\n\t\tt.Fatalf(\"Unlock: inconsistent state: %v\", err)\n\t}\n}\n\nfunc TestMemLSExpiry(t *testing.T) {\n\tm := NewMemLS().(*memLS)\n\ttestCases := []string{\n\t\t\"setNow 0\",\n\t\t\"create /a.5\",\n\t\t\"want /a.5\",\n\t\t\"create /c.6\",\n\t\t\"want /a.5 /c.6\",\n\t\t\"create /a/b.7\",\n\t\t\"want /a.5 /a/b.7 /c.6\",\n\t\t\"setNow 4\",\n\t\t\"want /a.5 /a/b.7 /c.6\",\n\t\t\"setNow 5\",\n\t\t\"want /a/b.7 /c.6\",\n\t\t\"setNow 6\",\n\t\t\"want /a/b.7\",\n\t\t\"setNow 7\",\n\t\t\"want \",\n\t\t\"setNow 8\",\n\t\t\"want \",\n\t\t\"create /a.12\",\n\t\t\"create /b.13\",\n\t\t\"create /c.15\",\n\t\t\"create /a/d.16\",\n\t\t\"want /a.12 /a/d.16 /b.13 /c.15\",\n\t\t\"refresh /a.14\",\n\t\t\"want /a.14 /a/d.16 /b.13 /c.15\",\n\t\t\"setNow 12\",\n\t\t\"want /a.14 /a/d.16 /b.13 /c.15\",\n\t\t\"setNow 13\",\n\t\t\"want /a.14 /a/d.16 /c.15\",\n\t\t\"setNow 14\",\n\t\t\"want /a/d.16 /c.15\",\n\t\t\"refresh /a/d.20\",\n\t\t\"refresh /c.20\",\n\t\t\"want /a/d.20 /c.20\",\n\t\t\"setNow 20\",\n\t\t\"want \",\n\t}\n\n\ttokens := map[string]string{}\n\tzTime := time.Unix(0, 0)\n\tnow := zTime\n\tfor i, tc := range testCases {\n\t\tj := strings.IndexByte(tc, ' ')\n\t\tif j < 0 {\n\t\t\tt.Fatalf(\"test case #%d %q: invalid command\", i, tc)\n\t\t}\n\t\top, arg := tc[:j], tc[j+1:]\n\t\tswitch op {\n\t\tdefault:\n\t\t\tt.Fatalf(\"test case #%d %q: invalid operation %q\", i, tc, op)\n\n\t\tcase \"create\", \"refresh\":\n\t\t\tparts := strings.Split(arg, \".\")\n\t\t\tif len(parts) != 2 {\n\t\t\t\tt.Fatalf(\"test case #%d %q: invalid create\", i, tc)\n\t\t\t}\n\t\t\troot := parts[0]\n\t\t\td, err := strconv.Atoi(parts[1])\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"test case #%d %q: invalid duration\", i, tc)\n\t\t\t}\n\t\t\tdur := time.Unix(0, 0).Add(time.Duration(d) * time.Second).Sub(now)\n\n\t\t\tswitch op {\n\t\t\tcase \"create\":\n\t\t\t\ttoken, err := m.Create(now, LockDetails{\n\t\t\t\t\tRoot:      root,\n\t\t\t\t\tDuration:  dur,\n\t\t\t\t\tZeroDepth: true,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"test case #%d %q: Create: %v\", i, tc, err)\n\t\t\t\t}\n\t\t\t\ttokens[root] = token\n\n\t\t\tcase \"refresh\":\n\t\t\t\ttoken := tokens[root]\n\t\t\t\tif token == \"\" {\n\t\t\t\t\tt.Fatalf(\"test case #%d %q: no token for %q\", i, tc, root)\n\t\t\t\t}\n\t\t\t\tgot, err := m.Refresh(now, token, dur)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"test case #%d %q: Refresh: %v\", i, tc, err)\n\t\t\t\t}\n\t\t\t\twant := LockDetails{\n\t\t\t\t\tRoot:      root,\n\t\t\t\t\tDuration:  dur,\n\t\t\t\t\tZeroDepth: true,\n\t\t\t\t}\n\t\t\t\tif got != want {\n\t\t\t\t\tt.Fatalf(\"test case #%d %q:\\ngot  %v\\nwant %v\", i, tc, got, want)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase \"setNow\":\n\t\t\td, err := strconv.Atoi(arg)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"test case #%d %q: invalid duration\", i, tc)\n\t\t\t}\n\t\t\tnow = time.Unix(0, 0).Add(time.Duration(d) * time.Second)\n\n\t\tcase \"want\":\n\t\t\tm.mu.Lock()\n\t\t\tm.collectExpiredNodes(now)\n\t\t\tgot := make([]string, 0, len(m.byToken))\n\t\t\tfor _, n := range m.byToken {\n\t\t\t\tgot = append(got, fmt.Sprintf(\"%s.%d\",\n\t\t\t\t\tn.details.Root, n.expiry.Sub(zTime)/time.Second))\n\t\t\t}\n\t\t\tm.mu.Unlock()\n\t\t\tsort.Strings(got)\n\t\t\twant := []string{}\n\t\t\tif arg != \"\" {\n\t\t\t\twant = strings.Split(arg, \" \")\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"test case #%d %q:\\ngot  %q\\nwant %q\", i, tc, got, want)\n\t\t\t}\n\t\t}\n\n\t\tif err := m.consistent(); err != nil {\n\t\t\tt.Fatalf(\"test case #%d %q: inconsistent state: %v\", i, tc, err)\n\t\t}\n\t}\n}\n\nfunc TestMemLS(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tm := NewMemLS().(*memLS)\n\trng := rand.New(rand.NewSource(0))\n\ttokens := map[string]string{}\n\tnConfirm, nCreate, nRefresh, nUnlock := 0, 0, 0, 0\n\tconst N = 2000\n\n\tfor i := 0; i < N; i++ {\n\t\tname := lockTestNames[rng.Intn(len(lockTestNames))]\n\t\tduration := lockTestDurations[rng.Intn(len(lockTestDurations))]\n\t\tconfirmed, unlocked := false, false\n\n\t\t// If the name was already locked, we randomly confirm/release, refresh\n\t\t// or unlock it. Otherwise, we create a lock.\n\t\ttoken := tokens[name]\n\t\tif token != \"\" {\n\t\t\tswitch rng.Intn(3) {\n\t\t\tcase 0:\n\t\t\t\tconfirmed = true\n\t\t\t\tnConfirm++\n\t\t\t\trelease, err := m.Confirm(now, name, \"\", Condition{Token: token})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"iteration #%d: Confirm %q: %v\", i, name, err)\n\t\t\t\t}\n\t\t\t\tif err := m.consistent(); err != nil {\n\t\t\t\t\tt.Fatalf(\"iteration #%d: inconsistent state: %v\", i, err)\n\t\t\t\t}\n\t\t\t\trelease()\n\n\t\t\tcase 1:\n\t\t\t\tnRefresh++\n\t\t\t\tif _, err := m.Refresh(now, token, duration); err != nil {\n\t\t\t\t\tt.Fatalf(\"iteration #%d: Refresh %q: %v\", i, name, err)\n\t\t\t\t}\n\n\t\t\tcase 2:\n\t\t\t\tunlocked = true\n\t\t\t\tnUnlock++\n\t\t\t\tif err := m.Unlock(now, token); err != nil {\n\t\t\t\t\tt.Fatalf(\"iteration #%d: Unlock %q: %v\", i, name, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\t\t\tnCreate++\n\t\t\tvar err error\n\t\t\ttoken, err = m.Create(now, LockDetails{\n\t\t\t\tRoot:      name,\n\t\t\t\tDuration:  duration,\n\t\t\t\tZeroDepth: lockTestZeroDepth(name),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"iteration #%d: Create %q: %v\", i, name, err)\n\t\t\t}\n\t\t}\n\n\t\tif !confirmed {\n\t\t\tif duration == 0 || unlocked {\n\t\t\t\t// A zero-duration lock should expire immediately and is\n\t\t\t\t// effectively equivalent to being unlocked.\n\t\t\t\ttokens[name] = \"\"\n\t\t\t} else {\n\t\t\t\ttokens[name] = token\n\t\t\t}\n\t\t}\n\n\t\tif err := m.consistent(); err != nil {\n\t\t\tt.Fatalf(\"iteration #%d: inconsistent state: %v\", i, err)\n\t\t}\n\t}\n\n\tif nConfirm < N/10 {\n\t\tt.Fatalf(\"too few Confirm calls: got %d, want >= %d\", nConfirm, N/10)\n\t}\n\tif nCreate < N/10 {\n\t\tt.Fatalf(\"too few Create calls: got %d, want >= %d\", nCreate, N/10)\n\t}\n\tif nRefresh < N/10 {\n\t\tt.Fatalf(\"too few Refresh calls: got %d, want >= %d\", nRefresh, N/10)\n\t}\n\tif nUnlock < N/10 {\n\t\tt.Fatalf(\"too few Unlock calls: got %d, want >= %d\", nUnlock, N/10)\n\t}\n}\n\nfunc (m *memLS) consistent() error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\t// If m.byName is non-empty, then it must contain an entry for the root \"/\",\n\t// and its refCount should equal the number of locked nodes.\n\tif len(m.byName) > 0 {\n\t\tn := m.byName[\"/\"]\n\t\tif n == nil {\n\t\t\treturn fmt.Errorf(`non-empty m.byName does not contain the root \"/\"`)\n\t\t}\n\t\tif n.refCount != len(m.byToken) {\n\t\t\treturn fmt.Errorf(\"root node refCount=%d, differs from len(m.byToken)=%d\", n.refCount, len(m.byToken))\n\t\t}\n\t}\n\n\tfor name, n := range m.byName {\n\t\t// The map keys should be consistent with the node's copy of the key.\n\t\tif n.details.Root != name {\n\t\t\treturn fmt.Errorf(\"node name %q != byName map key %q\", n.details.Root, name)\n\t\t}\n\n\t\t// A name must be clean, and start with a \"/\".\n\t\tif len(name) == 0 || name[0] != '/' {\n\t\t\treturn fmt.Errorf(`node name %q does not start with \"/\"`, name)\n\t\t}\n\t\tif name != path.Clean(name) {\n\t\t\treturn fmt.Errorf(`node name %q is not clean`, name)\n\t\t}\n\n\t\t// A node's refCount should be positive.\n\t\tif n.refCount <= 0 {\n\t\t\treturn fmt.Errorf(\"non-positive refCount for node at name %q\", name)\n\t\t}\n\n\t\t// A node's refCount should be the number of self-or-descendents that\n\t\t// are locked (i.e. have a non-empty token).\n\t\tvar list []string\n\t\tfor name0, n0 := range m.byName {\n\t\t\t// All of lockTestNames' name fragments are one byte long: '_', 'i' or 'z',\n\t\t\t// so strings.HasPrefix is equivalent to self-or-descendent name match.\n\t\t\t// We don't have to worry about \"/foo/bar\" being a false positive match\n\t\t\t// for \"/foo/b\".\n\t\t\tif strings.HasPrefix(name0, name) && n0.token != \"\" {\n\t\t\t\tlist = append(list, name0)\n\t\t\t}\n\t\t}\n\t\tif n.refCount != len(list) {\n\t\t\tsort.Strings(list)\n\t\t\treturn fmt.Errorf(\"node at name %q has refCount %d but locked self-or-descendents are %q (len=%d)\",\n\t\t\t\tname, n.refCount, list, len(list))\n\t\t}\n\n\t\t// A node n is in m.byToken if it has a non-empty token.\n\t\tif n.token != \"\" {\n\t\t\tif _, ok := m.byToken[n.token]; !ok {\n\t\t\t\treturn fmt.Errorf(\"node at name %q has token %q but not in m.byToken\", name, n.token)\n\t\t\t}\n\t\t}\n\n\t\t// A node n is in m.byExpiry if it has a non-negative byExpiryIndex.\n\t\tif n.byExpiryIndex >= 0 {\n\t\t\tif n.byExpiryIndex >= len(m.byExpiry) {\n\t\t\t\treturn fmt.Errorf(\"node at name %q has byExpiryIndex %d but m.byExpiry has length %d\", name, n.byExpiryIndex, len(m.byExpiry))\n\t\t\t}\n\t\t\tif n != m.byExpiry[n.byExpiryIndex] {\n\t\t\t\treturn fmt.Errorf(\"node at name %q has byExpiryIndex %d but that indexes a different node\", name, n.byExpiryIndex)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor token, n := range m.byToken {\n\t\t// The map keys should be consistent with the node's copy of the key.\n\t\tif n.token != token {\n\t\t\treturn fmt.Errorf(\"node token %q != byToken map key %q\", n.token, token)\n\t\t}\n\n\t\t// Every node in m.byToken is in m.byName.\n\t\tif _, ok := m.byName[n.details.Root]; !ok {\n\t\t\treturn fmt.Errorf(\"node at name %q in m.byToken but not in m.byName\", n.details.Root)\n\t\t}\n\t}\n\n\tfor i, n := range m.byExpiry {\n\t\t// The slice indices should be consistent with the node's copy of the index.\n\t\tif n.byExpiryIndex != i {\n\t\t\treturn fmt.Errorf(\"node byExpiryIndex %d != byExpiry slice index %d\", n.byExpiryIndex, i)\n\t\t}\n\n\t\t// Every node in m.byExpiry is in m.byName.\n\t\tif _, ok := m.byName[n.details.Root]; !ok {\n\t\t\treturn fmt.Errorf(\"node at name %q in m.byExpiry but not in m.byName\", n.details.Root)\n\t\t}\n\n\t\t// No node in m.byExpiry should be held.\n\t\tif n.held {\n\t\t\treturn fmt.Errorf(\"node at name %q in m.byExpiry is held\", n.details.Root)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc TestParseTimeout(t *testing.T) {\n\ttestCases := []struct {\n\t\ts       string\n\t\twant    time.Duration\n\t\twantErr error\n\t}{{\n\t\t\"\",\n\t\tinfiniteTimeout,\n\t\tnil,\n\t}, {\n\t\t\"Infinite\",\n\t\tinfiniteTimeout,\n\t\tnil,\n\t}, {\n\t\t\"Infinitesimal\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"infinite\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second-0\",\n\t\t0 * time.Second,\n\t\tnil,\n\t}, {\n\t\t\"Second-123\",\n\t\t123 * time.Second,\n\t\tnil,\n\t}, {\n\t\t\"  Second-456    \",\n\t\t456 * time.Second,\n\t\tnil,\n\t}, {\n\t\t\"Second-4100000000\",\n\t\t4100000000 * time.Second,\n\t\tnil,\n\t}, {\n\t\t\"junk\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second-\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second--1\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second--123\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second-+123\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second-0x123\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"second-123\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t\"Second-4294967295\",\n\t\t4294967295 * time.Second,\n\t\tnil,\n\t}, {\n\t\t// Section 10.7 says that \"The timeout value for TimeType \"Second\"\n\t\t// must not be greater than 2^32-1.\"\n\t\t\"Second-4294967296\",\n\t\t0,\n\t\terrInvalidTimeout,\n\t}, {\n\t\t// This test case comes from section 9.10.9 of the spec. It says,\n\t\t//\n\t\t// \"In this request, the client has specified that it desires an\n\t\t// infinite-length lock, if available, otherwise a timeout of 4.1\n\t\t// billion seconds, if available.\"\n\t\t//\n\t\t// The Go WebDAV package always supports infinite length locks,\n\t\t// and ignores the fallback after the comma.\n\t\t\"Infinite, Second-4100000000\",\n\t\tinfiniteTimeout,\n\t\tnil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tgot, gotErr := parseTimeout(tc.s)\n\t\tif got != tc.want || gotErr != tc.wantErr {\n\t\t\tt.Errorf(\"parsing %q:\\ngot  %v, %v\\nwant %v, %v\", tc.s, got, gotErr, tc.want, tc.wantErr)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/webdav/prop.go",
    "content": "// Copyright 2015 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\n// Proppatch describes a property update instruction as defined in RFC 4918.\n// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH\ntype Proppatch struct {\n\t// Remove specifies whether this patch removes properties. If it does not\n\t// remove them, it sets them.\n\tRemove bool\n\t// Props contains the properties to be set or removed.\n\tProps []Property\n}\n\n// Propstat describes a XML propstat element as defined in RFC 4918.\n// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat\ntype Propstat struct {\n\t// Props contains the properties for which Status applies.\n\tProps []Property\n\n\t// Status defines the HTTP status code of the properties in Prop.\n\t// Allowed values include, but are not limited to the WebDAV status\n\t// code extensions for HTTP/1.1.\n\t// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11\n\tStatus int\n\n\t// XMLError contains the XML representation of the optional error element.\n\t// XML content within this field must not rely on any predefined\n\t// namespace declarations or prefixes. If empty, the XML error element\n\t// is omitted.\n\tXMLError string\n\n\t// ResponseDescription contains the contents of the optional\n\t// responsedescription field. If empty, the XML element is omitted.\n\tResponseDescription string\n}\n\n// makePropstats returns a slice containing those of x and y whose Props slice\n// is non-empty. If both are empty, it returns a slice containing an otherwise\n// zero Propstat whose HTTP status code is 200 OK.\nfunc makePropstats(x, y Propstat) []Propstat {\n\tpstats := make([]Propstat, 0, 2)\n\tif len(x.Props) != 0 {\n\t\tpstats = append(pstats, x)\n\t}\n\tif len(y.Props) != 0 {\n\t\tpstats = append(pstats, y)\n\t}\n\tif len(pstats) == 0 {\n\t\tpstats = append(pstats, Propstat{\n\t\t\tStatus: http.StatusOK,\n\t\t})\n\t}\n\treturn pstats\n}\n\n// DeadPropsHolder holds the dead properties of a resource.\n//\n// Dead properties are those properties that are explicitly defined. In\n// comparison, live properties, such as DAV:getcontentlength, are implicitly\n// defined by the underlying resource, and cannot be explicitly overridden or\n// removed. See the Terminology section of\n// http://www.webdav.org/specs/rfc4918.html#rfc.section.3\n//\n// There is a whitelist of the names of live properties. This package handles\n// all live properties, and will only pass non-whitelisted names to the Patch\n// method of DeadPropsHolder implementations.\ntype DeadPropsHolder interface {\n\t// DeadProps returns a copy of the dead properties held.\n\tDeadProps() (map[xml.Name]Property, error)\n\n\t// Patch patches the dead properties held.\n\t//\n\t// Patching is atomic; either all or no patches succeed. It returns (nil,\n\t// non-nil) if an internal server error occurred, otherwise the Propstats\n\t// collectively contain one Property for each proposed patch Property. If\n\t// all patches succeed, Patch returns a slice of length one and a Propstat\n\t// element with a 200 OK HTTP status code. If none succeed, for reasons\n\t// other than an internal server error, no Propstat has status 200 OK.\n\t//\n\t// For more details on when various HTTP status codes apply, see\n\t// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status\n\tPatch([]Proppatch) ([]Propstat, error)\n}\n\n// liveProps contains all supported properties.\nvar liveProps = map[xml.Name]struct {\n\t// findFn implements the propfind function of this property. If nil,\n\t// it indicates a hidden property.\n\tfindFn func(context.Context, LockSystem, string, model.Obj) (string, error)\n\t// dir is true if the property applies to directories.\n\tdir bool\n}{\n\t{Space: \"DAV:\", Local: \"resourcetype\"}: {\n\t\tfindFn: findResourceType,\n\t\tdir:    true,\n\t},\n\t{Space: \"DAV:\", Local: \"displayname\"}: {\n\t\tfindFn: findDisplayName,\n\t\tdir:    true,\n\t},\n\t{Space: \"DAV:\", Local: \"getcontentlength\"}: {\n\t\tfindFn: findContentLength,\n\t\tdir:    false,\n\t},\n\t{Space: \"DAV:\", Local: \"getlastmodified\"}: {\n\t\tfindFn: findLastModified,\n\t\t// http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified\n\t\t// suggests that getlastmodified should only apply to GETable\n\t\t// resources, and this package does not support GET on directories.\n\t\t//\n\t\t// Nonetheless, some WebDAV clients expect child directories to be\n\t\t// sortable by getlastmodified date, so this value is true, not false.\n\t\t// See golang.org/issue/15334.\n\t\tdir: true,\n\t},\n\t{Space: \"DAV:\", Local: \"creationdate\"}: {\n\t\tfindFn: findCreationDate,\n\t\tdir:    true,\n\t},\n\t{Space: \"DAV:\", Local: \"getcontentlanguage\"}: {\n\t\tfindFn: nil,\n\t\tdir:    false,\n\t},\n\t{Space: \"DAV:\", Local: \"getcontenttype\"}: {\n\t\tfindFn: findContentType,\n\t\tdir:    false,\n\t},\n\t{Space: \"DAV:\", Local: \"getetag\"}: {\n\t\tfindFn: findETag,\n\t\t// findETag implements ETag as the concatenated hex values of a file's\n\t\t// modification time and size. This is not a reliable synchronization\n\t\t// mechanism for directories, so we do not advertise getetag for DAV\n\t\t// collections.\n\t\tdir: false,\n\t},\n\n\t// TODO: The lockdiscovery property requires LockSystem to list the\n\t// active locks on a resource.\n\t{Space: \"DAV:\", Local: \"lockdiscovery\"}: {},\n\t{Space: \"DAV:\", Local: \"supportedlock\"}: {\n\t\tfindFn: findSupportedLock,\n\t\tdir:    true,\n\t},\n\t{Space: \"http://owncloud.org/ns\", Local: \"checksums\"}: {\n\t\tfindFn: findChecksums,\n\t\tdir:    false,\n\t},\n}\n\n// TODO(nigeltao) merge props and allprop?\n\n// Props returns the status of the properties named pnames for resource name.\n//\n// Each Propstat has a unique status and each property name will only be part\n// of one Propstat element.\nfunc props(ctx context.Context, ls LockSystem, fi model.Obj, pnames []xml.Name) ([]Propstat, error) {\n\t//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//defer f.Close()\n\t//fi, err := f.Stat()\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\tisDir := fi.IsDir()\n\n\tvar deadProps map[xml.Name]Property\n\t// ??? what is this for?\n\t//if dph, ok := f.(DeadPropsHolder); ok {\n\t//\tdeadProps, err = dph.DeadProps()\n\t//\tif err != nil {\n\t//\t\treturn nil, err\n\t//\t}\n\t//}\n\n\tpstatOK := Propstat{Status: http.StatusOK}\n\tpstatNotFound := Propstat{Status: http.StatusNotFound}\n\tfor _, pn := range pnames {\n\t\t// If this file has dead properties, check if they contain pn.\n\t\tif dp, ok := deadProps[pn]; ok {\n\t\t\tpstatOK.Props = append(pstatOK.Props, dp)\n\t\t\tcontinue\n\t\t}\n\t\t// Otherwise, it must either be a live property or we don't know it.\n\t\tif prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {\n\t\t\tinnerXML, err := prop.findFn(ctx, ls, fi.GetName(), fi)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpstatOK.Props = append(pstatOK.Props, Property{\n\t\t\t\tXMLName:  pn,\n\t\t\t\tInnerXML: []byte(innerXML),\n\t\t\t})\n\t\t} else {\n\t\t\tpstatNotFound.Props = append(pstatNotFound.Props, Property{\n\t\t\t\tXMLName: pn,\n\t\t\t})\n\t\t}\n\t}\n\treturn makePropstats(pstatOK, pstatNotFound), nil\n}\n\n// Propnames returns the property names defined for resource name.\nfunc propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, error) {\n\t//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//defer f.Close()\n\t//fi, err := f.Stat()\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\tisDir := fi.IsDir()\n\n\tvar deadProps map[xml.Name]Property\n\t// ??? what is this for?\n\t//if dph, ok := f.(DeadPropsHolder); ok {\n\t//\tdeadProps, err = dph.DeadProps()\n\t//\tif err != nil {\n\t//\t\treturn nil, err\n\t//\t}\n\t//}\n\n\tpnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))\n\tfor pn, prop := range liveProps {\n\t\tif prop.findFn != nil && (prop.dir || !isDir) {\n\t\t\tpnames = append(pnames, pn)\n\t\t}\n\t}\n\tfor pn := range deadProps {\n\t\tpnames = append(pnames, pn)\n\t}\n\treturn pnames, nil\n}\n\n// Allprop returns the properties defined for resource name and the properties\n// named in include.\n//\n// Note that RFC 4918 defines 'allprop' to return the DAV: properties defined\n// within the RFC plus dead properties. Other live properties should only be\n// returned if they are named in 'include'.\n//\n// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND\nfunc allprop(ctx context.Context, ls LockSystem, fi model.Obj, include []xml.Name) ([]Propstat, error) {\n\tpnames, err := propnames(ctx, ls, fi)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Add names from include if they are not already covered in pnames.\n\tnameset := make(map[xml.Name]bool)\n\tfor _, pn := range pnames {\n\t\tnameset[pn] = true\n\t}\n\tfor _, pn := range include {\n\t\tif !nameset[pn] {\n\t\t\tpnames = append(pnames, pn)\n\t\t}\n\t}\n\treturn props(ctx, ls, fi, pnames)\n}\n\n// Patch patches the properties of resource name. The return values are\n// constrained in the same manner as DeadPropsHolder.Patch.\nfunc patch(ctx context.Context, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {\n\tconflict := false\nloop:\n\tfor _, patch := range patches {\n\t\tfor _, p := range patch.Props {\n\t\t\tif _, ok := liveProps[p.XMLName]; ok {\n\t\t\t\tconflict = true\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t}\n\t}\n\tif conflict {\n\t\tpstatForbidden := Propstat{\n\t\t\tStatus:   http.StatusForbidden,\n\t\t\tXMLError: `<D:cannot-modify-protected-property xmlns:D=\"DAV:\"/>`,\n\t\t}\n\t\tpstatFailedDep := Propstat{\n\t\t\tStatus: StatusFailedDependency,\n\t\t}\n\t\tfor _, patch := range patches {\n\t\t\tfor _, p := range patch.Props {\n\t\t\t\tif _, ok := liveProps[p.XMLName]; ok {\n\t\t\t\t\tpstatForbidden.Props = append(pstatForbidden.Props, Property{XMLName: p.XMLName})\n\t\t\t\t} else {\n\t\t\t\t\tpstatFailedDep.Props = append(pstatFailedDep.Props, Property{XMLName: p.XMLName})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn makePropstats(pstatForbidden, pstatFailedDep), nil\n\t}\n\n\t// ------------------------------------------------------------\n\t//f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//defer f.Close()\n\t//if dph, ok := f.(DeadPropsHolder); ok {\n\t//\tret, err := dph.Patch(patches)\n\t//\tif err != nil {\n\t//\t\treturn nil, err\n\t//\t}\n\t//\t// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat says that\n\t//\t// \"The contents of the prop XML element must only list the names of\n\t//\t// properties to which the result in the status element applies.\"\n\t//\tfor _, pstat := range ret {\n\t//\t\tfor i, p := range pstat.Props {\n\t//\t\t\tpstat.Props[i] = Property{XMLName: p.XMLName}\n\t//\t\t}\n\t//\t}\n\t//\treturn ret, nil\n\t//}\n\t// ------------------------------------------------------------\n\n\t// The file doesn't implement the optional DeadPropsHolder interface, so\n\t// all patches are forbidden.\n\tpstat := Propstat{Status: http.StatusForbidden}\n\tfor _, patch := range patches {\n\t\tfor _, p := range patch.Props {\n\t\t\tpstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})\n\t\t}\n\t}\n\treturn []Propstat{pstat}, nil\n}\n\nfunc escapeXML(s string) string {\n\tfor i := 0; i < len(s); i++ {\n\t\t// As an optimization, if s contains only ASCII letters, digits or a\n\t\t// few special characters, the escaped value is s itself and we don't\n\t\t// need to allocate a buffer and convert between string and []byte.\n\t\tswitch c := s[i]; {\n\t\tcase c == ' ' || c == '_' ||\n\t\t\t('+' <= c && c <= '9') || // Digits as well as + , - . and /\n\t\t\t('A' <= c && c <= 'Z') ||\n\t\t\t('a' <= c && c <= 'z'):\n\t\t\tcontinue\n\t\t}\n\t\t// Otherwise, go through the full escaping process.\n\t\tvar buf bytes.Buffer\n\t\txml.EscapeText(&buf, []byte(s))\n\t\treturn buf.String()\n\t}\n\treturn s\n}\n\nfunc findResourceType(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\tif fi.IsDir() {\n\t\treturn `<D:collection xmlns:D=\"DAV:\"/>`, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc findDisplayName(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\tif slashClean(name) == \"/\" {\n\t\t// Hide the real name of a possibly prefixed root directory.\n\t\treturn \"\", nil\n\t}\n\treturn escapeXML(fi.GetName()), nil\n}\n\nfunc findContentLength(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\treturn strconv.FormatInt(fi.GetSize(), 10), nil\n}\n\nfunc findLastModified(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\treturn fi.ModTime().UTC().Format(http.TimeFormat), nil\n}\nfunc findCreationDate(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\tuserAgent := ctx.Value(conf.UserAgentKey).(string)\n\tif strings.Contains(strings.ToLower(userAgent), \"microsoft-webdav\") {\n\t\treturn fi.CreateTime().UTC().Format(http.TimeFormat), nil\n\t}\n\treturn fi.CreateTime().UTC().Format(time.RFC3339), nil\n}\n\n// ErrNotImplemented should be returned by optional interfaces if they\n// want the original implementation to be used.\nvar ErrNotImplemented = errors.New(\"not implemented\")\n\n// ContentTyper is an optional interface for the os.FileInfo\n// objects returned by the FileSystem.\n//\n// If this interface is defined then it will be used to read the\n// content type from the object.\n//\n// If this interface is not defined the file will be opened and the\n// content type will be guessed from the initial contents of the file.\ntype ContentTyper interface {\n\t// ContentType returns the content type for the file.\n\t//\n\t// If this returns error ErrNotImplemented then the error will\n\t// be ignored and the base implementation will be used\n\t// instead.\n\tContentType(ctx context.Context) (string, error)\n}\n\nfunc findContentType(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\t//if do, ok := fi.(ContentTyper); ok {\n\t//\tctype, err := do.ContentType(ctx)\n\t//\tif err != ErrNotImplemented {\n\t//\t\treturn ctype, err\n\t//\t}\n\t//}\n\t//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)\n\t//if err != nil {\n\t//\treturn \"\", err\n\t//}\n\t//defer f.Close()\n\t// This implementation is based on serveContent's code in the standard net/http package.\n\tctype := utils.GetMimeType(name)\n\treturn ctype, nil\n\t//if ctype != \"\" {\n\t//\treturn ctype, nil\n\t//}\n\t//return \"application/octet-stream\", nil\n\t// Read a chunk to decide between utf-8 text and binary.\n\t//var buf [512]byte\n\t//n, err := io.ReadFull(f, buf[:])\n\t//if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t//\treturn \"\", err\n\t//}\n\t//ctype = http.DetectContentType(buf[:n])\n\t//// Rewind file.\n\t//_, err = f.Seek(0, os.SEEK_SET)\n\t//return ctype, err\n}\n\n// ETager is an optional interface for the os.FileInfo objects\n// returned by the FileSystem.\n//\n// If this interface is defined then it will be used to read the ETag\n// for the object.\n//\n// If this interface is not defined an ETag will be computed using the\n// ModTime() and the Size() methods of the os.FileInfo object.\ntype ETager interface {\n\t// ETag returns an ETag for the file.  This should be of the\n\t// form \"value\" or W/\"value\"\n\t//\n\t// If this returns error ErrNotImplemented then the error will\n\t// be ignored and the base implementation will be used\n\t// instead.\n\tETag(ctx context.Context) (string, error)\n}\n\nfunc findETag(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\tif do, ok := fi.(ETager); ok {\n\t\tetag, err := do.ETag(ctx)\n\t\tif !errors.Is(err, ErrNotImplemented) {\n\t\t\treturn etag, err\n\t\t}\n\t}\n\treturn common.GetEtag(fi, fi.GetSize()), nil\n}\n\nfunc findSupportedLock(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\treturn `` +\n\t\t`<D:lockentry xmlns:D=\"DAV:\">` +\n\t\t`<D:lockscope><D:exclusive/></D:lockscope>` +\n\t\t`<D:locktype><D:write/></D:locktype>` +\n\t\t`</D:lockentry>`, nil\n}\n\nfunc findChecksums(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {\n\tchecksums := \"\"\n\tfor hashType, hashValue := range fi.GetHash().All() {\n\t\tchecksums += fmt.Sprintf(\"<checksum>%s:%s</checksum>\", hashType.Name, hashValue)\n\t}\n\treturn checksums, nil\n}\n"
  },
  {
    "path": "server/webdav/util.go",
    "content": "package webdav\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\nfunc (h *Handler) getModTime(r *http.Request) time.Time {\n\treturn h.getHeaderTime(r, \"X-OC-Mtime\", \"\")\n}\n\n// owncloud/ nextcloud haven't impl this, but we can add the support since rclone may support this soon.\n// try ModTime if CreateTime not found in header\nfunc (h *Handler) getCreateTime(r *http.Request) time.Time {\n\treturn h.getHeaderTime(r, \"X-OC-Ctime\", \"X-OC-Mtime\")\n}\n\nfunc (h *Handler) getHeaderTime(r *http.Request, header, alternative string) time.Time {\n\thVal := r.Header.Get(header)\n\t// try alternative\n\tif hVal == \"\" && alternative != \"\" {\n\t\thVal = r.Header.Get(alternative)\n\t}\n\tif hVal != \"\" {\n\t\tmodTimeUnix, err := strconv.ParseInt(hVal, 10, 64)\n\t\tif err == nil {\n\t\t\treturn time.Unix(modTimeUnix, 0)\n\t\t}\n\t\tlog.Warnf(\"getModTime in Webdav, failed to parse %s, %s\", header, err)\n\t}\n\treturn time.Now()\n}\n"
  },
  {
    "path": "server/webdav/webdav.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package webdav provides a WebDAV server implementation.\npackage webdav // import \"golang.org/x/net/webdav\"\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/net\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/errs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/fs\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n)\n\ntype Handler struct {\n\t// Prefix is the URL path prefix to strip from WebDAV resource paths.\n\tPrefix string\n\t// LockSystem is the lock management system.\n\tLockSystem LockSystem\n\t// Logger is an optional error logger. If non-nil, it will be called\n\t// for all HTTP requests.\n\tLogger func(*http.Request, error)\n}\n\nfunc (h *Handler) stripPrefix(p string) (string, int, error) {\n\tif h.Prefix == \"\" {\n\t\treturn p, http.StatusOK, nil\n\t}\n\tif r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) {\n\t\treturn r, http.StatusOK, nil\n\t}\n\treturn p, http.StatusNotFound, errPrefixMismatch\n}\n\nfunc (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tstatus, err := http.StatusBadRequest, errUnsupportedMethod\n\tbrw := newBufferedResponseWriter()\n\tuseBufferedWriter := true\n\tif h.LockSystem == nil {\n\t\tstatus, err = http.StatusInternalServerError, errNoLockSystem\n\t} else {\n\t\tswitch r.Method {\n\t\tcase \"OPTIONS\":\n\t\t\tstatus, err = h.handleOptions(brw, r)\n\t\tcase \"GET\", \"HEAD\", \"POST\":\n\t\t\tuseBufferedWriter = false\n\t\t\tWriter := &common.WrittenResponseWriter{ResponseWriter: w}\n\t\t\tstatus, err = h.handleGetHeadPost(Writer, r)\n\t\t\tif status != 0 && Writer.IsWritten() {\n\t\t\t\tstatus = 0\n\t\t\t}\n\t\tcase \"DELETE\":\n\t\t\tstatus, err = h.handleDelete(brw, r)\n\t\tcase \"PUT\":\n\t\t\tstatus, err = h.handlePut(brw, r)\n\t\tcase \"MKCOL\":\n\t\t\tstatus, err = h.handleMkcol(brw, r)\n\t\tcase \"COPY\", \"MOVE\":\n\t\t\tstatus, err = h.handleCopyMove(brw, r)\n\t\tcase \"LOCK\":\n\t\t\tstatus, err = h.handleLock(brw, r)\n\t\tcase \"UNLOCK\":\n\t\t\tstatus, err = h.handleUnlock(brw, r)\n\t\tcase \"PROPFIND\":\n\t\t\tstatus, err = h.handlePropfind(brw, r)\n\t\t\t// if there is a error for PROPFIND, we should be as an empty folder to the client\n\t\t\tif err != nil {\n\t\t\t\tstatus = http.StatusNotFound\n\t\t\t}\n\t\tcase \"PROPPATCH\":\n\t\t\tstatus, err = h.handleProppatch(brw, r)\n\t\t}\n\t}\n\n\tif status != 0 {\n\t\tw.WriteHeader(status)\n\t\tif status != http.StatusNoContent {\n\t\t\tw.Write([]byte(StatusText(status)))\n\t\t}\n\t} else if useBufferedWriter {\n\t\tbrw.WriteToResponse(w)\n\t}\n\tif h.Logger != nil && err != nil {\n\t\th.Logger(r, err)\n\t}\n}\n\nfunc (h *Handler) lock(now time.Time, root string) (token string, status int, err error) {\n\ttoken, err = h.LockSystem.Create(now, LockDetails{\n\t\tRoot:      root,\n\t\tDuration:  infiniteTimeout,\n\t\tZeroDepth: true,\n\t})\n\tif err != nil {\n\t\tif err == ErrLocked {\n\t\t\treturn \"\", StatusLocked, err\n\t\t}\n\t\treturn \"\", http.StatusInternalServerError, err\n\t}\n\treturn token, 0, nil\n}\n\nfunc (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {\n\thdr := r.Header.Get(\"If\")\n\tif hdr == \"\" {\n\t\t// An empty If header means that the client hasn't previously created locks.\n\t\t// Even if this client doesn't care about locks, we still need to check that\n\t\t// the resources aren't locked by another client, so we create temporary\n\t\t// locks that would conflict with another client's locks. These temporary\n\t\t// locks are unlocked at the end of the HTTP request.\n\t\tnow, srcToken, dstToken := time.Now(), \"\", \"\"\n\t\tif src != \"\" {\n\t\t\tsrcToken, status, err = h.lock(now, src)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status, err\n\t\t\t}\n\t\t}\n\t\tif dst != \"\" {\n\t\t\tdstToken, status, err = h.lock(now, dst)\n\t\t\tif err != nil {\n\t\t\t\tif srcToken != \"\" {\n\t\t\t\t\th.LockSystem.Unlock(now, srcToken)\n\t\t\t\t}\n\t\t\t\treturn nil, status, err\n\t\t\t}\n\t\t}\n\n\t\treturn func() {\n\t\t\tif dstToken != \"\" {\n\t\t\t\th.LockSystem.Unlock(now, dstToken)\n\t\t\t}\n\t\t\tif srcToken != \"\" {\n\t\t\t\th.LockSystem.Unlock(now, srcToken)\n\t\t\t}\n\t\t}, 0, nil\n\t}\n\n\tih, ok := parseIfHeader(hdr)\n\tif !ok {\n\t\treturn nil, http.StatusBadRequest, errInvalidIfHeader\n\t}\n\t// ih is a disjunction (OR) of ifLists, so any ifList will do.\n\tfor _, l := range ih.lists {\n\t\tlsrc := l.resourceTag\n\t\tif lsrc == \"\" {\n\t\t\tlsrc = src\n\t\t} else {\n\t\t\tu, err := url.Parse(lsrc)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif u.Host != r.Host {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlsrc, status, err = h.stripPrefix(u.Path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status, err\n\t\t\t}\n\t\t}\n\t\trelease, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...)\n\t\tif err == ErrConfirmationFailed {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, http.StatusInternalServerError, err\n\t\t}\n\t\treturn release, 0, nil\n\t}\n\t// Section 10.4.1 says that \"If this header is evaluated and all state lists\n\t// fail, then the request must fail with a 412 (Precondition Failed) status.\"\n\t// We follow the spec even though the cond_put_corrupt_token test case from\n\t// the litmus test warns on seeing a 412 instead of a 423 (Locked).\n\treturn nil, http.StatusPreconditionFailed, ErrLocked\n}\n\nfunc (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\tallow := \"OPTIONS, LOCK, PUT, MKCOL\"\n\tif fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil {\n\t\tif fi.IsDir() {\n\t\t\tallow = \"OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND\"\n\t\t} else {\n\t\t\tallow = \"OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT\"\n\t\t}\n\t}\n\tw.Header().Set(\"Allow\", allow)\n\t// http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes\n\tw.Header().Set(\"DAV\", \"1, 2\")\n\t// http://msdn.microsoft.com/en-au/library/cc250217.aspx\n\tw.Header().Set(\"MS-Author-Via\", \"DAV\")\n\treturn 0, nil\n}\n\nfunc (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\t// TODO: check locks for read-only access??\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn http.StatusForbidden, err\n\t}\n\tfi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})\n\tif err != nil {\n\t\treturn http.StatusNotFound, err\n\t}\n\tif fi.IsDir() {\n\t\tif r.Method == http.MethodHead {\n\t\t\tw.Header().Set(\"Content-Type\", \"httpd/unix-directory\")\n\t\t\tw.Header().Set(\"Content-Length\", \"0\")\n\t\t\treturn http.StatusOK, nil\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, nil\n\t}\n\t// Let ServeContent determine the Content-Type header.\n\tstorage, _ := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})\n\tif storage.GetStorage().Webdav302() {\n\t\tlink, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, Redirect: true})\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\tdefer link.Close()\n\t\thttp.Redirect(w, r, link.URL, http.StatusFound)\n\t\treturn 0, nil\n\t}\n\n\tif storage.GetStorage().WebdavProxyURL() {\n\t\tif url := common.GenerateDownProxyURL(storage.GetStorage(), reqPath); url != \"\" {\n\t\t\tw.Header().Set(\"Cache-Control\", \"max-age=0, no-cache, no-store, must-revalidate\")\n\t\t\thttp.Redirect(w, r, url, http.StatusFound)\n\t\t\treturn 0, nil\n\t\t}\n\t}\n\n\tlink, _, err := fs.Link(ctx, reqPath, model.LinkArgs{Header: r.Header})\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\tdefer link.Close()\n\n\tif storage.GetStorage().ProxyRange {\n\t\tlink = common.ProxyRange(ctx, link, fi.GetSize())\n\t}\n\terr = common.Proxy(w, r, link, fi)\n\tif err != nil {\n\t\tif statusCode, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {\n\t\t\treturn int(statusCode), err\n\t\t}\n\t\treturn http.StatusInternalServerError, fmt.Errorf(\"webdav proxy error: %+v\", err)\n\t}\n\treturn 0, nil\n}\n\nfunc (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\trelease, status, err := h.confirmLocks(r, reqPath, \"\")\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tdefer release()\n\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\t// TODO: return MultiStatus where appropriate.\n\n\t// \"godoc os RemoveAll\" says that \"If the path does not exist, RemoveAll\n\t// returns nil (no error).\" WebDAV semantics are that it should return a\n\t// \"404 Not Found\". We therefore have to Stat before we RemoveAll.\n\tif _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil {\n\t\tif errs.IsObjectNotFound(err) {\n\t\t\treturn http.StatusNotFound, err\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\tif err := fs.Remove(ctx, reqPath); err != nil {\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\t//fs.ClearCache(path.Dir(reqPath))\n\treturn http.StatusNoContent, nil\n}\n\nfunc (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\tdefer func() {\n\t\tif n, _ := io.ReadFull(r.Body, []byte{0}); n == 1 {\n\t\t\t_, _ = utils.CopyWithBuffer(io.Discard, r.Body)\n\t\t}\n\t\t_ = r.Body.Close()\n\t}()\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tif reqPath == \"\" {\n\t\treturn http.StatusMethodNotAllowed, nil\n\t}\n\trelease, status, err := h.confirmLocks(r, reqPath, \"\")\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tdefer release()\n\t// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'\n\t// comments in http.checkEtag.\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn http.StatusForbidden, err\n\t}\n\tsize := r.ContentLength\n\tif size < 0 {\n\t\tsizeStr := r.Header.Get(\"X-File-Size\")\n\t\tif sizeStr != \"\" {\n\t\t\tsize, err = strconv.ParseInt(sizeStr, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn http.StatusBadRequest, err\n\t\t\t}\n\t\t}\n\t}\n\tobj := model.Object{\n\t\tName:     path.Base(reqPath),\n\t\tSize:     size,\n\t\tModified: h.getModTime(r),\n\t\tCtime:    h.getCreateTime(r),\n\t}\n\t// Check if system file should be ignored\n\tif setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(obj.Name) {\n\t\treturn http.StatusForbidden, errs.IgnoredSystemFile\n\t}\n\tfsStream := &stream.FileStream{\n\t\tObj:      &obj,\n\t\tReader:   r.Body,\n\t\tMimetype: r.Header.Get(\"Content-Type\"),\n\t}\n\tif fsStream.Mimetype == \"\" {\n\t\tfsStream.Mimetype = utils.GetMimeType(reqPath)\n\t}\n\terr = fs.PutDirectly(ctx, path.Dir(reqPath), fsStream)\n\tif errs.IsNotFoundError(err) {\n\t\treturn http.StatusNotFound, err\n\t}\n\n\t// TODO(rost): Returning 405 Method Not Allowed might not be appropriate.\n\tif err != nil {\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\tfi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})\n\tif err != nil {\n\t\tfi = &obj\n\t}\n\tetag, err := findETag(ctx, h.LockSystem, reqPath, fi)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\tw.Header().Set(\"Etag\", etag)\n\treturn http.StatusCreated, nil\n}\n\nfunc (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\trelease, status, err := h.confirmLocks(r, reqPath, \"\")\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tdefer release()\n\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\n\tif r.ContentLength > 0 {\n\t\treturn http.StatusUnsupportedMediaType, nil\n\t}\n\n\t// RFC 4918 9.3.1\n\t//405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL\n\tif _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil {\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\t// RFC 4918 9.3.1\n\t// 409 (Conflict) The server MUST NOT create those intermediate collections automatically.\n\treqDir := path.Dir(reqPath)\n\tif _, err := fs.Get(ctx, reqDir, &fs.GetArgs{}); err != nil {\n\t\tif errs.IsObjectNotFound(err) {\n\t\t\treturn http.StatusConflict, err\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\tif err := fs.MakeDir(ctx, reqPath); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn http.StatusConflict, err\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\treturn http.StatusCreated, nil\n}\n\nfunc (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\thdr := r.Header.Get(\"Destination\")\n\tif hdr == \"\" {\n\t\treturn http.StatusBadRequest, errInvalidDestination\n\t}\n\tu, err := url.Parse(hdr)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, errInvalidDestination\n\t}\n\tif u.Host != \"\" && u.Host != r.Host {\n\t\treturn http.StatusBadGateway, errInvalidDestination\n\t}\n\n\tsrc, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\n\tdst, status, err := h.stripPrefix(u.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\n\tif dst == \"\" {\n\t\treturn http.StatusBadGateway, errInvalidDestination\n\t}\n\tif dst == src {\n\t\treturn http.StatusForbidden, errDestinationEqualsSource\n\t}\n\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\tsrc, err = user.JoinPath(src)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\tdst, err = user.JoinPath(dst)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\n\tif r.Method == \"COPY\" {\n\t\t// Section 7.5.1 says that a COPY only needs to lock the destination,\n\t\t// not both destination and source. Strictly speaking, this is racy,\n\t\t// even though a COPY doesn't modify the source, if a concurrent\n\t\t// operation modifies the source. However, the litmus test explicitly\n\t\t// checks that COPYing a locked-by-another source is OK.\n\t\trelease, status, err := h.confirmLocks(r, \"\", dst)\n\t\tif err != nil {\n\t\t\treturn status, err\n\t\t}\n\t\tdefer release()\n\n\t\t// Section 9.8.3 says that \"The COPY method on a collection without a Depth\n\t\t// header must act as if a Depth header with value \"infinity\" was included\".\n\t\tdepth := infiniteDepth\n\t\tif hdr := r.Header.Get(\"Depth\"); hdr != \"\" {\n\t\t\tdepth = parseDepth(hdr)\n\t\t\tif depth != 0 && depth != infiniteDepth {\n\t\t\t\t// Section 9.8.3 says that \"A client may submit a Depth header on a\n\t\t\t\t// COPY on a collection with a value of \"0\" or \"infinity\".\"\n\t\t\t\treturn http.StatusBadRequest, errInvalidDepth\n\t\t\t}\n\t\t}\n\t\treturn copyFiles(ctx, src, dst, r.Header.Get(\"Overwrite\") != \"F\")\n\t}\n\n\trelease, status, err := h.confirmLocks(r, src, dst)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tdefer release()\n\n\t// Section 9.9.2 says that \"The MOVE method on a collection must act as if\n\t// a \"Depth: infinity\" header was used on it. A client must not submit a\n\t// Depth header on a MOVE on a collection with any value but \"infinity\".\"\n\tif hdr := r.Header.Get(\"Depth\"); hdr != \"\" {\n\t\tif parseDepth(hdr) != infiniteDepth {\n\t\t\treturn http.StatusBadRequest, errInvalidDepth\n\t\t}\n\t}\n\treturn moveFiles(ctx, src, dst, r.Header.Get(\"Overwrite\") == \"T\")\n}\n\nfunc (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {\n\tduration, err := parseTimeout(r.Header.Get(\"Timeout\"))\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\tli, status, err := readLockInfo(r.Body)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\ttoken, ld, now, created := \"\", LockDetails{}, time.Now(), false\n\tif li == (lockInfo{}) {\n\t\t// An empty lockInfo means to refresh the lock.\n\t\tih, ok := parseIfHeader(r.Header.Get(\"If\"))\n\t\tif !ok {\n\t\t\treturn http.StatusBadRequest, errInvalidIfHeader\n\t\t}\n\t\tif len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {\n\t\t\ttoken = ih.lists[0].conditions[0].Token\n\t\t}\n\t\tif token == \"\" {\n\t\t\treturn http.StatusBadRequest, errInvalidLockToken\n\t\t}\n\t\tld, err = h.LockSystem.Refresh(now, token, duration)\n\t\tif err != nil {\n\t\t\tif err == ErrNoSuchLock {\n\t\t\t\treturn http.StatusPreconditionFailed, err\n\t\t\t}\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\n\t} else {\n\t\t// Section 9.10.3 says that \"If no Depth header is submitted on a LOCK request,\n\t\t// then the request MUST act as if a \"Depth:infinity\" had been submitted.\"\n\t\tdepth := infiniteDepth\n\t\tif hdr := r.Header.Get(\"Depth\"); hdr != \"\" {\n\t\t\tdepth = parseDepth(hdr)\n\t\t\tif depth != 0 && depth != infiniteDepth {\n\t\t\t\t// Section 9.10.3 says that \"Values other than 0 or infinity must not be\n\t\t\t\t// used with the Depth header on a LOCK method\".\n\t\t\t\treturn http.StatusBadRequest, errInvalidDepth\n\t\t\t}\n\t\t}\n\t\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\t\tif err != nil {\n\t\t\treturn status, err\n\t\t}\n\t\treqPath, err = user.JoinPath(reqPath)\n\t\tif err != nil {\n\t\t\treturn 403, err\n\t\t}\n\t\tld = LockDetails{\n\t\t\tRoot:      reqPath,\n\t\t\tDuration:  duration,\n\t\t\tOwnerXML:  li.Owner.InnerXML,\n\t\t\tZeroDepth: depth == 0,\n\t\t}\n\t\ttoken, err = h.LockSystem.Create(now, ld)\n\t\tif err != nil {\n\t\t\tif err == ErrLocked {\n\t\t\t\treturn StatusLocked, err\n\t\t\t}\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\tdefer func() {\n\t\t\tif retErr != nil {\n\t\t\t\th.LockSystem.Unlock(now, token)\n\t\t\t}\n\t\t}()\n\n\t\t// ??? Why create resource here?\n\t\t//// Create the resource if it didn't previously exist.\n\t\t//if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {\n\t\t//\tf, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)\n\t\t//\tif err != nil {\n\t\t//\t\t// TODO: detect missing intermediate dirs and return http.StatusConflict?\n\t\t//\t\treturn http.StatusInternalServerError, err\n\t\t//\t}\n\t\t//\tf.Close()\n\t\t//\tcreated = true\n\t\t//}\n\n\t\t// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the\n\t\t// Lock-Token value is a Coded-URL. We add angle brackets.\n\t\tw.Header().Set(\"Lock-Token\", \"<\"+token+\">\")\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/xml; charset=utf-8\")\n\tif created {\n\t\t// This is \"w.WriteHeader(http.StatusCreated)\" and not \"return\n\t\t// http.StatusCreated, nil\" because we write our own (XML) response to w\n\t\t// and Handler.ServeHTTP would otherwise write \"Created\".\n\t\tw.WriteHeader(http.StatusCreated)\n\t}\n\twriteLockInfo(w, token, ld)\n\treturn 0, nil\n}\n\nfunc (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\t// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the\n\t// Lock-Token value is a Coded-URL. We strip its angle brackets.\n\tt := r.Header.Get(\"Lock-Token\")\n\tif len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {\n\t\treturn http.StatusBadRequest, errInvalidLockToken\n\t}\n\tt = t[1 : len(t)-1]\n\n\tswitch err = h.LockSystem.Unlock(time.Now(), t); err {\n\tcase nil:\n\t\treturn http.StatusNoContent, err\n\tcase ErrForbidden:\n\t\treturn http.StatusForbidden, err\n\tcase ErrLocked:\n\t\treturn StatusLocked, err\n\tcase ErrNoSuchLock:\n\t\treturn http.StatusConflict, err\n\tdefault:\n\t\treturn http.StatusInternalServerError, err\n\t}\n}\n\nfunc (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tctx := r.Context()\n\tuserAgent := r.Header.Get(\"User-Agent\")\n\tctx = context.WithValue(ctx, conf.UserAgentKey, userAgent)\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\tfi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})\n\tif err != nil {\n\t\tif errs.IsNotFoundError(err) {\n\t\t\treturn http.StatusNotFound, err\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\tdepth := infiniteDepth\n\tif hdr := r.Header.Get(\"Depth\"); hdr != \"\" {\n\t\tdepth = parseDepth(hdr)\n\t\tif depth == invalidDepth {\n\t\t\treturn http.StatusBadRequest, errInvalidDepth\n\t\t}\n\t}\n\tpf, status, err := readPropfind(r.Body)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\n\tmw := multistatusWriter{w: w}\n\n\twalkFn := func(reqPath string, info model.Obj, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar pstats []Propstat\n\t\tif pf.Propname != nil {\n\t\t\tpnames, err := propnames(ctx, h.LockSystem, info)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tpstat := Propstat{Status: http.StatusOK}\n\t\t\tfor _, xmlname := range pnames {\n\t\t\t\tpstat.Props = append(pstat.Props, Property{XMLName: xmlname})\n\t\t\t}\n\t\t\tpstats = append(pstats, pstat)\n\t\t} else if pf.Allprop != nil {\n\t\t\tpstats, err = allprop(ctx, h.LockSystem, info, pf.Prop)\n\t\t} else {\n\t\t\tpstats, err = props(ctx, h.LockSystem, info, pf.Prop)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\thref := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))\n\t\tif href != \"/\" && info.IsDir() {\n\t\t\thref += \"/\"\n\t\t}\n\t\treturn mw.write(makePropstatResponse(href, pstats))\n\t}\n\n\twalkErr := walkFS(ctx, depth, reqPath, fi, walkFn)\n\tcloseErr := mw.close()\n\tif walkErr != nil {\n\t\treturn http.StatusInternalServerError, walkErr\n\t}\n\tif closeErr != nil {\n\t\treturn http.StatusInternalServerError, closeErr\n\t}\n\treturn 0, nil\n}\n\nfunc (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {\n\treqPath, status, err := h.stripPrefix(r.URL.Path)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\trelease, status, err := h.confirmLocks(r, reqPath, \"\")\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tdefer release()\n\n\tctx := r.Context()\n\tuser := ctx.Value(conf.UserKey).(*model.User)\n\treqPath, err = user.JoinPath(reqPath)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\tif _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil {\n\t\tif errs.IsObjectNotFound(err) {\n\t\t\treturn http.StatusNotFound, err\n\t\t}\n\t\treturn http.StatusMethodNotAllowed, err\n\t}\n\tpatches, status, err := readProppatch(r.Body)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\tpstats, err := patch(ctx, h.LockSystem, reqPath, patches)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\tmw := multistatusWriter{w: w}\n\twriteErr := mw.write(makePropstatResponse(r.URL.Path, pstats))\n\tcloseErr := mw.close()\n\tif writeErr != nil {\n\t\treturn http.StatusInternalServerError, writeErr\n\t}\n\tif closeErr != nil {\n\t\treturn http.StatusInternalServerError, closeErr\n\t}\n\treturn 0, nil\n}\n\nfunc makePropstatResponse(href string, pstats []Propstat) *response {\n\tresp := response{\n\t\tHref:     []string{(&url.URL{Path: href}).EscapedPath()},\n\t\tPropstat: make([]propstat, 0, len(pstats)),\n\t}\n\tfor _, p := range pstats {\n\t\tvar xmlErr *xmlError\n\t\tif p.XMLError != \"\" {\n\t\t\txmlErr = &xmlError{InnerXML: []byte(p.XMLError)}\n\t\t}\n\t\tresp.Propstat = append(resp.Propstat, propstat{\n\t\t\tStatus:              fmt.Sprintf(\"HTTP/1.1 %d %s\", p.Status, StatusText(p.Status)),\n\t\t\tProp:                p.Props,\n\t\t\tResponseDescription: p.ResponseDescription,\n\t\t\tError:               xmlErr,\n\t\t})\n\t}\n\treturn &resp\n}\n\nconst (\n\tinfiniteDepth = -1\n\tinvalidDepth  = -2\n)\n\n// parseDepth maps the strings \"0\", \"1\" and \"infinity\" to 0, 1 and\n// infiniteDepth. Parsing any other string returns invalidDepth.\n//\n// Different WebDAV methods have further constraints on valid depths:\n//   - PROPFIND has no further restrictions, as per section 9.1.\n//   - COPY accepts only \"0\" or \"infinity\", as per section 9.8.3.\n//   - MOVE accepts only \"infinity\", as per section 9.9.2.\n//   - LOCK accepts only \"0\" or \"infinity\", as per section 9.10.3.\n//\n// These constraints are enforced by the handleXxx methods.\nfunc parseDepth(s string) int {\n\tswitch s {\n\tcase \"0\":\n\t\treturn 0\n\tcase \"1\":\n\t\treturn 1\n\tcase \"infinity\":\n\t\treturn infiniteDepth\n\t}\n\treturn invalidDepth\n}\n\n// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11\nconst (\n\tStatusMulti               = 207\n\tStatusUnprocessableEntity = 422\n\tStatusLocked              = 423\n\tStatusFailedDependency    = 424\n\tStatusInsufficientStorage = 507\n)\n\nfunc StatusText(code int) string {\n\tswitch code {\n\tcase StatusMulti:\n\t\treturn \"Multi-Status\"\n\tcase StatusUnprocessableEntity:\n\t\treturn \"Unprocessable Entity\"\n\tcase StatusLocked:\n\t\treturn \"Locked\"\n\tcase StatusFailedDependency:\n\t\treturn \"Failed Dependency\"\n\tcase StatusInsufficientStorage:\n\t\treturn \"Insufficient Storage\"\n\t}\n\treturn http.StatusText(code)\n}\n\nvar (\n\terrDestinationEqualsSource = errors.New(\"webdav: destination equals source\")\n\terrDirectoryNotEmpty       = errors.New(\"webdav: directory not empty\")\n\terrInvalidDepth            = errors.New(\"webdav: invalid depth\")\n\terrInvalidDestination      = errors.New(\"webdav: invalid destination\")\n\terrInvalidIfHeader         = errors.New(\"webdav: invalid If header\")\n\terrInvalidLockInfo         = errors.New(\"webdav: invalid lock info\")\n\terrInvalidLockToken        = errors.New(\"webdav: invalid lock token\")\n\terrInvalidPropfind         = errors.New(\"webdav: invalid propfind\")\n\terrInvalidProppatch        = errors.New(\"webdav: invalid proppatch\")\n\terrInvalidResponse         = errors.New(\"webdav: invalid response\")\n\terrInvalidTimeout          = errors.New(\"webdav: invalid timeout\")\n\terrNoFileSystem            = errors.New(\"webdav: no file system\")\n\terrNoLockSystem            = errors.New(\"webdav: no lock system\")\n\terrNotADirectory           = errors.New(\"webdav: not a directory\")\n\terrPrefixMismatch          = errors.New(\"webdav: prefix mismatch\")\n\terrRecursionTooDeep        = errors.New(\"webdav: recursion too deep\")\n\terrUnsupportedLockInfo     = errors.New(\"webdav: unsupported lock info\")\n\terrUnsupportedMethod       = errors.New(\"webdav: unsupported method\")\n)\n"
  },
  {
    "path": "server/webdav/xml.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\n// The XML encoding is covered by Section 14.\n// http://www.webdav.org/specs/rfc4918.html#xml.element.definitions\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t// As of https://go-review.googlesource.com/#/c/12772/ which was submitted\n\t// in July 2015, this package uses an internal fork of the standard\n\t// library's encoding/xml package, due to changes in the way namespaces\n\t// were encoded. Such changes were introduced in the Go 1.5 cycle, but were\n\t// rolled back in response to https://github.com/golang/go/issues/11841\n\t//\n\t// However, this package's exported API, specifically the Property and\n\t// DeadPropsHolder types, need to refer to the standard library's version\n\t// of the xml.Name type, as code that imports this package cannot refer to\n\t// the internal version.\n\t//\n\t// This file therefore imports both the internal and external versions, as\n\t// ixml and xml, and converts between them.\n\t//\n\t// In the long term, this package should use the standard library's version\n\t// only, and the internal fork deleted, once\n\t// https://github.com/golang/go/issues/13400 is resolved.\n\tixml \"github.com/OpenListTeam/OpenList/v4/server/webdav/internal/xml\"\n)\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo\ntype lockInfo struct {\n\tXMLName   ixml.Name `xml:\"lockinfo\"`\n\tExclusive *struct{} `xml:\"lockscope>exclusive\"`\n\tShared    *struct{} `xml:\"lockscope>shared\"`\n\tWrite     *struct{} `xml:\"locktype>write\"`\n\tOwner     owner     `xml:\"owner\"`\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner\ntype owner struct {\n\tInnerXML string `xml:\",innerxml\"`\n}\n\nfunc readLockInfo(r io.Reader) (li lockInfo, status int, err error) {\n\tc := &countingReader{r: r}\n\tif err = ixml.NewDecoder(c).Decode(&li); err != nil {\n\t\tif err == io.EOF {\n\t\t\tif c.n == 0 {\n\t\t\t\t// An empty body means to refresh the lock.\n\t\t\t\t// http://www.webdav.org/specs/rfc4918.html#refreshing-locks\n\t\t\t\treturn lockInfo{}, 0, nil\n\t\t\t}\n\t\t\terr = errInvalidLockInfo\n\t\t}\n\t\treturn lockInfo{}, http.StatusBadRequest, err\n\t}\n\t// We only support exclusive (non-shared) write locks. In practice, these are\n\t// the only types of locks that seem to matter.\n\tif li.Exclusive == nil || li.Shared != nil || li.Write == nil {\n\t\treturn lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo\n\t}\n\treturn li, 0, nil\n}\n\ntype countingReader struct {\n\tn int\n\tr io.Reader\n}\n\nfunc (c *countingReader) Read(p []byte) (int, error) {\n\tn, err := c.r.Read(p)\n\tc.n += n\n\treturn n, err\n}\n\nfunc writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {\n\tdepth := \"infinity\"\n\tif ld.ZeroDepth {\n\t\tdepth = \"0\"\n\t}\n\ttimeout := ld.Duration / time.Second\n\treturn fmt.Fprintf(w, \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?>\\n\"+\n\t\t\"<D:prop xmlns:D=\\\"DAV:\\\"><D:lockdiscovery><D:activelock>\\n\"+\n\t\t\"\t<D:locktype><D:write/></D:locktype>\\n\"+\n\t\t\"\t<D:lockscope><D:exclusive/></D:lockscope>\\n\"+\n\t\t\"\t<D:depth>%s</D:depth>\\n\"+\n\t\t\"\t<D:owner>%s</D:owner>\\n\"+\n\t\t\"\t<D:timeout>Second-%d</D:timeout>\\n\"+\n\t\t\"\t<D:locktoken><D:href>%s</D:href></D:locktoken>\\n\"+\n\t\t\"\t<D:lockroot><D:href>%s</D:href></D:lockroot>\\n\"+\n\t\t\"</D:activelock></D:lockdiscovery></D:prop>\",\n\t\tdepth, ld.OwnerXML, timeout, escape(token), escape(ld.Root),\n\t)\n}\n\nfunc escape(s string) string {\n\tfor i := 0; i < len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '\"', '&', '\\'', '<', '>':\n\t\t\tb := bytes.NewBuffer(nil)\n\t\t\tixml.EscapeText(b, []byte(s))\n\t\t\treturn b.String()\n\t\t}\n\t}\n\treturn s\n}\n\n// Next returns the next token, if any, in the XML stream of d.\n// RFC 4918 requires to ignore comments, processing instructions\n// and directives.\n// http://www.webdav.org/specs/rfc4918.html#property_values\n// http://www.webdav.org/specs/rfc4918.html#xml-extensibility\nfunc next(d *ixml.Decoder) (ixml.Token, error) {\n\tfor {\n\t\tt, err := d.Token()\n\t\tif err != nil {\n\t\t\treturn t, err\n\t\t}\n\t\tswitch t.(type) {\n\t\tcase ixml.Comment, ixml.Directive, ixml.ProcInst:\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn t, nil\n\t\t}\n\t}\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)\ntype propfindProps []xml.Name\n\n// UnmarshalXML appends the property names enclosed within start to pn.\n//\n// It returns an error if start does not contain any properties or if\n// properties contain values. Character data between properties is ignored.\nfunc (pn *propfindProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {\n\tfor {\n\t\tt, err := next(d)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch t.(type) {\n\t\tcase ixml.EndElement:\n\t\t\tif len(*pn) == 0 {\n\t\t\t\treturn fmt.Errorf(\"%s must not be empty\", start.Name.Local)\n\t\t\t}\n\t\t\treturn nil\n\t\tcase ixml.StartElement:\n\t\t\tname := t.(ixml.StartElement).Name\n\t\t\tt, err = next(d)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, ok := t.(ixml.EndElement); !ok {\n\t\t\t\treturn fmt.Errorf(\"unexpected token %T\", t)\n\t\t\t}\n\t\t\t*pn = append(*pn, xml.Name(name))\n\t\t}\n\t}\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind\ntype propfind struct {\n\tXMLName  ixml.Name     `xml:\"DAV: propfind\"`\n\tAllprop  *struct{}     `xml:\"DAV: allprop\"`\n\tPropname *struct{}     `xml:\"DAV: propname\"`\n\tProp     propfindProps `xml:\"DAV: prop\"`\n\tInclude  propfindProps `xml:\"DAV: include\"`\n}\n\nfunc readPropfind(r io.Reader) (pf propfind, status int, err error) {\n\tc := countingReader{r: r}\n\tif err = ixml.NewDecoder(&c).Decode(&pf); err != nil {\n\t\tif err == io.EOF {\n\t\t\tif c.n == 0 {\n\t\t\t\t// An empty body means to propfind allprop.\n\t\t\t\t// http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND\n\t\t\t\treturn propfind{Allprop: new(struct{})}, 0, nil\n\t\t\t}\n\t\t\terr = errInvalidPropfind\n\t\t}\n\t\treturn propfind{}, http.StatusBadRequest, err\n\t}\n\n\tif pf.Allprop == nil && pf.Include != nil {\n\t\treturn propfind{}, http.StatusBadRequest, errInvalidPropfind\n\t}\n\tif pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) {\n\t\treturn propfind{}, http.StatusBadRequest, errInvalidPropfind\n\t}\n\tif pf.Prop != nil && pf.Propname != nil {\n\t\treturn propfind{}, http.StatusBadRequest, errInvalidPropfind\n\t}\n\tif pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil {\n\t\treturn propfind{}, http.StatusBadRequest, errInvalidPropfind\n\t}\n\treturn pf, 0, nil\n}\n\n// Property represents a single DAV resource property as defined in RFC 4918.\n// See http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties\ntype Property struct {\n\t// XMLName is the fully qualified name that identifies this property.\n\tXMLName xml.Name\n\n\t// Lang is an optional xml:lang attribute.\n\tLang string `xml:\"xml:lang,attr,omitempty\"`\n\n\t// InnerXML contains the XML representation of the property value.\n\t// See http://www.webdav.org/specs/rfc4918.html#property_values\n\t//\n\t// Property values of complex type or mixed-content must have fully\n\t// expanded XML namespaces or be self-contained with according\n\t// XML namespace declarations. They must not rely on any XML\n\t// namespace declarations within the scope of the XML document,\n\t// even including the DAV: namespace.\n\tInnerXML []byte `xml:\",innerxml\"`\n}\n\n// ixmlProperty is the same as the Property type except it holds an ixml.Name\n// instead of an xml.Name.\ntype ixmlProperty struct {\n\tXMLName  ixml.Name\n\tLang     string `xml:\"xml:lang,attr,omitempty\"`\n\tInnerXML []byte `xml:\",innerxml\"`\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error\n// See multistatusWriter for the \"D:\" namespace prefix.\ntype xmlError struct {\n\tXMLName  ixml.Name `xml:\"D:error\"`\n\tInnerXML []byte    `xml:\",innerxml\"`\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat\n// See multistatusWriter for the \"D:\" namespace prefix.\ntype propstat struct {\n\tProp                []Property `xml:\"D:prop>_ignored_\"`\n\tStatus              string     `xml:\"D:status\"`\n\tError               *xmlError  `xml:\"D:error\"`\n\tResponseDescription string     `xml:\"D:responsedescription,omitempty\"`\n}\n\n// ixmlPropstat is the same as the propstat type except it holds an ixml.Name\n// instead of an xml.Name.\ntype ixmlPropstat struct {\n\tProp                []ixmlProperty `xml:\"D:prop>_ignored_\"`\n\tStatus              string         `xml:\"D:status\"`\n\tError               *xmlError      `xml:\"D:error\"`\n\tResponseDescription string         `xml:\"D:responsedescription,omitempty\"`\n}\n\n// MarshalXML prepends the \"D:\" namespace prefix on properties in the DAV: namespace\n// before encoding. See multistatusWriter.\nfunc (ps propstat) MarshalXML(e *ixml.Encoder, start ixml.StartElement) error {\n\t// Convert from a propstat to an ixmlPropstat.\n\tixmlPs := ixmlPropstat{\n\t\tProp:                make([]ixmlProperty, len(ps.Prop)),\n\t\tStatus:              ps.Status,\n\t\tError:               ps.Error,\n\t\tResponseDescription: ps.ResponseDescription,\n\t}\n\tfor k, prop := range ps.Prop {\n\t\tixmlPs.Prop[k] = ixmlProperty{\n\t\t\tXMLName:  ixml.Name(prop.XMLName),\n\t\t\tLang:     prop.Lang,\n\t\t\tInnerXML: prop.InnerXML,\n\t\t}\n\t}\n\n\tfor k, prop := range ixmlPs.Prop {\n\t\tif prop.XMLName.Space == \"DAV:\" {\n\t\t\tprop.XMLName = ixml.Name{Space: \"\", Local: \"D:\" + prop.XMLName.Local}\n\t\t\tixmlPs.Prop[k] = prop\n\t\t}\n\t}\n\t// Distinct type to avoid infinite recursion of MarshalXML.\n\ttype newpropstat ixmlPropstat\n\treturn e.EncodeElement(newpropstat(ixmlPs), start)\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response\n// See multistatusWriter for the \"D:\" namespace prefix.\ntype response struct {\n\tXMLName             ixml.Name  `xml:\"D:response\"`\n\tHref                []string   `xml:\"D:href\"`\n\tPropstat            []propstat `xml:\"D:propstat\"`\n\tStatus              string     `xml:\"D:status,omitempty\"`\n\tError               *xmlError  `xml:\"D:error\"`\n\tResponseDescription string     `xml:\"D:responsedescription,omitempty\"`\n}\n\n// MultistatusWriter marshals one or more Responses into a XML\n// multistatus response.\n// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus\n// TODO(rsto, mpl): As a workaround, the \"D:\" namespace prefix, defined as\n// \"DAV:\" on this element, is prepended on the nested response, as well as on all\n// its nested elements. All property names in the DAV: namespace are prefixed as\n// well. This is because some versions of Mini-Redirector (on windows 7) ignore\n// elements with a default namespace (no prefixed namespace). A less intrusive fix\n// should be possible after golang.org/cl/11074. See https://golang.org/issue/11177\ntype multistatusWriter struct {\n\t// ResponseDescription contains the optional responsedescription\n\t// of the multistatus XML element. Only the latest content before\n\t// close will be emitted. Empty response descriptions are not\n\t// written.\n\tresponseDescription string\n\n\tw   http.ResponseWriter\n\tenc *ixml.Encoder\n}\n\n// Write validates and emits a DAV response as part of a multistatus response\n// element.\n//\n// It sets the HTTP status code of its underlying http.ResponseWriter to 207\n// (Multi-Status) and populates the Content-Type header. If r is the\n// first, valid response to be written, Write prepends the XML representation\n// of r with a multistatus tag. Callers must call close after the last response\n// has been written.\nfunc (w *multistatusWriter) write(r *response) error {\n\tswitch len(r.Href) {\n\tcase 0:\n\t\treturn errInvalidResponse\n\tcase 1:\n\t\tif len(r.Propstat) > 0 != (r.Status == \"\") {\n\t\t\treturn errInvalidResponse\n\t\t}\n\tdefault:\n\t\tif len(r.Propstat) > 0 || r.Status == \"\" {\n\t\t\treturn errInvalidResponse\n\t\t}\n\t}\n\terr := w.writeHeader()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn w.enc.Encode(r)\n}\n\n// writeHeader writes a XML multistatus start element on w's underlying\n// http.ResponseWriter and returns the result of the write operation.\n// After the first write attempt, writeHeader becomes a no-op.\nfunc (w *multistatusWriter) writeHeader() error {\n\tif w.enc != nil {\n\t\treturn nil\n\t}\n\tw.w.Header().Add(\"Content-Type\", \"text/xml; charset=utf-8\")\n\tw.w.WriteHeader(StatusMulti)\n\t_, err := fmt.Fprintf(w.w, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>`)\n\tif err != nil {\n\t\treturn err\n\t}\n\tw.enc = ixml.NewEncoder(w.w)\n\treturn w.enc.EncodeToken(ixml.StartElement{\n\t\tName: ixml.Name{\n\t\t\tSpace: \"DAV:\",\n\t\t\tLocal: \"multistatus\",\n\t\t},\n\t\tAttr: []ixml.Attr{{\n\t\t\tName:  ixml.Name{Space: \"xmlns\", Local: \"D\"},\n\t\t\tValue: \"DAV:\",\n\t\t}},\n\t})\n}\n\n// Close completes the marshalling of the multistatus response. It returns\n// an error if the multistatus response could not be completed. If both the\n// return value and field enc of w are nil, then no multistatus response has\n// been written.\nfunc (w *multistatusWriter) close() error {\n\tif w.enc == nil {\n\t\treturn nil\n\t}\n\tvar end []ixml.Token\n\tif w.responseDescription != \"\" {\n\t\tname := ixml.Name{Space: \"DAV:\", Local: \"responsedescription\"}\n\t\tend = append(end,\n\t\t\tixml.StartElement{Name: name},\n\t\t\tixml.CharData(w.responseDescription),\n\t\t\tixml.EndElement{Name: name},\n\t\t)\n\t}\n\tend = append(end, ixml.EndElement{\n\t\tName: ixml.Name{Space: \"DAV:\", Local: \"multistatus\"},\n\t})\n\tfor _, t := range end {\n\t\terr := w.enc.EncodeToken(t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn w.enc.Flush()\n}\n\nvar xmlLangName = ixml.Name{Space: \"http://www.w3.org/XML/1998/namespace\", Local: \"lang\"}\n\nfunc xmlLang(s ixml.StartElement, d string) string {\n\tfor _, attr := range s.Attr {\n\t\tif attr.Name == xmlLangName {\n\t\t\treturn attr.Value\n\t\t}\n\t}\n\treturn d\n}\n\ntype xmlValue []byte\n\nfunc (v *xmlValue) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {\n\t// The XML value of a property can be arbitrary, mixed-content XML.\n\t// To make sure that the unmarshalled value contains all required\n\t// namespaces, we encode all the property value XML tokens into a\n\t// buffer. This forces the encoder to redeclare any used namespaces.\n\tvar b bytes.Buffer\n\te := ixml.NewEncoder(&b)\n\tfor {\n\t\tt, err := next(d)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif e, ok := t.(ixml.EndElement); ok && e.Name == start.Name {\n\t\t\tbreak\n\t\t}\n\t\tif err = e.EncodeToken(t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\terr := e.Flush()\n\tif err != nil {\n\t\treturn err\n\t}\n\t*v = b.Bytes()\n\treturn nil\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)\ntype proppatchProps []Property\n\n// UnmarshalXML appends the property names and values enclosed within start\n// to ps.\n//\n// An xml:lang attribute that is defined either on the DAV:prop or property\n// name XML element is propagated to the property's Lang field.\n//\n// UnmarshalXML returns an error if start does not contain any properties or if\n// property values contain syntactically incorrect XML.\nfunc (ps *proppatchProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {\n\tlang := xmlLang(start, \"\")\n\tfor {\n\t\tt, err := next(d)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch elem := t.(type) {\n\t\tcase ixml.EndElement:\n\t\t\tif len(*ps) == 0 {\n\t\t\t\treturn fmt.Errorf(\"%s must not be empty\", start.Name.Local)\n\t\t\t}\n\t\t\treturn nil\n\t\tcase ixml.StartElement:\n\t\t\tp := Property{\n\t\t\t\tXMLName: xml.Name(t.(ixml.StartElement).Name),\n\t\t\t\tLang:    xmlLang(t.(ixml.StartElement), lang),\n\t\t\t}\n\t\t\terr = d.DecodeElement(((*xmlValue)(&p.InnerXML)), &elem)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t*ps = append(*ps, p)\n\t\t}\n\t}\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove\ntype setRemove struct {\n\tXMLName ixml.Name\n\tLang    string         `xml:\"xml:lang,attr,omitempty\"`\n\tProp    proppatchProps `xml:\"DAV: prop\"`\n}\n\n// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate\ntype propertyupdate struct {\n\tXMLName   ixml.Name   `xml:\"DAV: propertyupdate\"`\n\tLang      string      `xml:\"xml:lang,attr,omitempty\"`\n\tSetRemove []setRemove `xml:\",any\"`\n}\n\nfunc readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {\n\tvar pu propertyupdate\n\tif err = ixml.NewDecoder(r).Decode(&pu); err != nil {\n\t\treturn nil, http.StatusBadRequest, err\n\t}\n\tfor _, op := range pu.SetRemove {\n\t\tremove := false\n\t\tswitch op.XMLName {\n\t\tcase ixml.Name{Space: \"DAV:\", Local: \"set\"}:\n\t\t\t// No-op.\n\t\tcase ixml.Name{Space: \"DAV:\", Local: \"remove\"}:\n\t\t\tfor _, p := range op.Prop {\n\t\t\t\tif len(p.InnerXML) > 0 {\n\t\t\t\t\treturn nil, http.StatusBadRequest, errInvalidProppatch\n\t\t\t\t}\n\t\t\t}\n\t\t\tremove = true\n\t\tdefault:\n\t\t\treturn nil, http.StatusBadRequest, errInvalidProppatch\n\t\t}\n\t\tpatches = append(patches, Proppatch{Remove: remove, Props: op.Prop})\n\t}\n\treturn patches, 0, nil\n}\n"
  },
  {
    "path": "server/webdav/xml_test.go",
    "content": "// Copyright 2014 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage webdav\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\tixml \"github.com/OpenListTeam/OpenList/v4/server/webdav/internal/xml\"\n)\n\nfunc TestReadLockInfo(t *testing.T) {\n\t// The \"section x.y.z\" test cases come from section x.y.z of the spec at\n\t// http://www.webdav.org/specs/rfc4918.html\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tinput      string\n\t\twantLI     lockInfo\n\t\twantStatus int\n\t}{{\n\t\t\"bad: junk\",\n\t\t\"xxx\",\n\t\tlockInfo{},\n\t\thttp.StatusBadRequest,\n\t}, {\n\t\t\"bad: invalid owner XML\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\" +\n\t\t\t\"  <D:owner>\\n\" +\n\t\t\t\"    <D:href>   no end tag   \\n\" +\n\t\t\t\"  </D:owner>\\n\" +\n\t\t\t\"</D:lockinfo>\",\n\t\tlockInfo{},\n\t\thttp.StatusBadRequest,\n\t}, {\n\t\t\"bad: invalid UTF-8\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\" +\n\t\t\t\"  <D:owner>\\n\" +\n\t\t\t\"    <D:href>   \\xff   </D:href>\\n\" +\n\t\t\t\"  </D:owner>\\n\" +\n\t\t\t\"</D:lockinfo>\",\n\t\tlockInfo{},\n\t\thttp.StatusBadRequest,\n\t}, {\n\t\t\"bad: unfinished XML #1\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\",\n\t\tlockInfo{},\n\t\thttp.StatusBadRequest,\n\t}, {\n\t\t\"bad: unfinished XML #2\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\" +\n\t\t\t\"  <D:owner>\\n\",\n\t\tlockInfo{},\n\t\thttp.StatusBadRequest,\n\t}, {\n\t\t\"good: empty\",\n\t\t\"\",\n\t\tlockInfo{},\n\t\t0,\n\t}, {\n\t\t\"good: plain-text owner\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\" +\n\t\t\t\"  <D:owner>gopher</D:owner>\\n\" +\n\t\t\t\"</D:lockinfo>\",\n\t\tlockInfo{\n\t\t\tXMLName:   ixml.Name{Space: \"DAV:\", Local: \"lockinfo\"},\n\t\t\tExclusive: new(struct{}),\n\t\t\tWrite:     new(struct{}),\n\t\t\tOwner: owner{\n\t\t\t\tInnerXML: \"gopher\",\n\t\t\t},\n\t\t},\n\t\t0,\n\t}, {\n\t\t\"section 9.10.7\",\n\t\t\"\" +\n\t\t\t\"<D:lockinfo xmlns:D='DAV:'>\\n\" +\n\t\t\t\"  <D:lockscope><D:exclusive/></D:lockscope>\\n\" +\n\t\t\t\"  <D:locktype><D:write/></D:locktype>\\n\" +\n\t\t\t\"  <D:owner>\\n\" +\n\t\t\t\"    <D:href>http://example.org/~ejw/contact.html</D:href>\\n\" +\n\t\t\t\"  </D:owner>\\n\" +\n\t\t\t\"</D:lockinfo>\",\n\t\tlockInfo{\n\t\t\tXMLName:   ixml.Name{Space: \"DAV:\", Local: \"lockinfo\"},\n\t\t\tExclusive: new(struct{}),\n\t\t\tWrite:     new(struct{}),\n\t\t\tOwner: owner{\n\t\t\t\tInnerXML: \"\\n    <D:href>http://example.org/~ejw/contact.html</D:href>\\n  \",\n\t\t\t},\n\t\t},\n\t\t0,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tli, status, err := readLockInfo(strings.NewReader(tc.input))\n\t\tif tc.wantStatus != 0 {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"%s: got nil error, want non-nil\", tc.desc)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if err != nil {\n\t\t\tt.Errorf(\"%s: %v\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {\n\t\t\tt.Errorf(\"%s:\\ngot  lockInfo=%v, status=%v\\nwant lockInfo=%v, status=%v\",\n\t\t\t\ttc.desc, li, status, tc.wantLI, tc.wantStatus)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc TestReadPropfind(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tinput      string\n\t\twantPF     propfind\n\t\twantStatus int\n\t}{{\n\t\tdesc: \"propfind: propname\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:propname/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName:  ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tPropname: new(struct{}),\n\t\t},\n\t}, {\n\t\tdesc:  \"propfind: empty body means allprop\",\n\t\tinput: \"\",\n\t\twantPF: propfind{\n\t\t\tAllprop: new(struct{}),\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: allprop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"   <A:allprop/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tAllprop: new(struct{}),\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: allprop followed by include\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:allprop/>\\n\" +\n\t\t\t\"  <A:include><A:displayname/></A:include>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tAllprop: new(struct{}),\n\t\t\tInclude: propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: include followed by allprop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:include><A:displayname/></A:include>\\n\" +\n\t\t\t\"  <A:allprop/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tAllprop: new(struct{}),\n\t\t\tInclude: propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: propfind\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:displayname/></A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tProp:    propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: prop with ignored comments\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop>\\n\" +\n\t\t\t\"    <!-- ignore -->\\n\" +\n\t\t\t\"    <A:displayname><!-- ignore --></A:displayname>\\n\" +\n\t\t\t\"  </A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tProp:    propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: propfind with ignored whitespace\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop>   <A:displayname/></A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tProp:    propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: propfind with ignored mixed-content\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop>foo<A:displayname/>bar</A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName: ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tProp:    propfindProps{xml.Name{Space: \"DAV:\", Local: \"displayname\"}},\n\t\t},\n\t}, {\n\t\tdesc: \"propfind: propname with ignored element (section A.4)\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:propname/>\\n\" +\n\t\t\t\"  <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantPF: propfind{\n\t\t\tXMLName:  ixml.Name{Space: \"DAV:\", Local: \"propfind\"},\n\t\t\tPropname: new(struct{}),\n\t\t},\n\t}, {\n\t\tdesc:       \"propfind: bad: junk\",\n\t\tinput:      \"xxx\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: propname and allprop (section A.3)\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:propname/>\" +\n\t\t\t\"  <A:allprop/>\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: propname and prop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:displayname/></A:prop>\\n\" +\n\t\t\t\"  <A:propname/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: allprop and prop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:allprop/>\\n\" +\n\t\t\t\"  <A:prop><A:foo/><A:/prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: empty propfind with ignored element (section A.4)\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <E:expired-props/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: empty prop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop/>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: prop with just chardata\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop>foo</A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"bad: interrupted prop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:foo></A:prop>\\n\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"bad: malformed end element prop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:foo/></A:bar></A:prop>\\n\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: property with chardata value\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:foo>bar</A:foo></A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: property with whitespace value\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:prop><A:foo> </A:foo></A:prop>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"propfind: bad: include without allprop\",\n\t\tinput: \"\" +\n\t\t\t\"<A:propfind xmlns:A='DAV:'>\\n\" +\n\t\t\t\"  <A:include><A:foo/></A:include>\\n\" +\n\t\t\t\"</A:propfind>\",\n\t\twantStatus: http.StatusBadRequest,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tpf, status, err := readPropfind(strings.NewReader(tc.input))\n\t\tif tc.wantStatus != 0 {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"%s: got nil error, want non-nil\", tc.desc)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if err != nil {\n\t\t\tt.Errorf(\"%s: %v\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {\n\t\t\tt.Errorf(\"%s:\\ngot  propfind=%v, status=%v\\nwant propfind=%v, status=%v\",\n\t\t\t\ttc.desc, pf, status, tc.wantPF, tc.wantStatus)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc TestMultistatusWriter(t *testing.T) {\n\t///The \"section x.y.z\" test cases come from section x.y.z of the spec at\n\t// http://www.webdav.org/specs/rfc4918.html\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tresponses   []response\n\t\trespdesc    string\n\t\twriteHeader bool\n\t\twantXML     string\n\t\twantCode    int\n\t\twantErr     error\n\t}{{\n\t\tdesc: \"section 9.2.2 (failed dependency)\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\"http://example.com/foo\"},\n\t\t\tPropstat: []propstat{{\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\tSpace: \"http://ns.example.com/\",\n\t\t\t\t\t\tLocal: \"Authors\",\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 424 Failed Dependency\",\n\t\t\t}, {\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\tSpace: \"http://ns.example.com/\",\n\t\t\t\t\t\tLocal: \"Copyright-Owner\",\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 409 Conflict\",\n\t\t\t}},\n\t\t\tResponseDescription: \"Copyright Owner cannot be deleted or altered.\",\n\t\t}},\n\t\twantXML: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"UTF-8\"?>` +\n\t\t\t`<multistatus xmlns=\"DAV:\">` +\n\t\t\t`  <response>` +\n\t\t\t`    <href>http://example.com/foo</href>` +\n\t\t\t`    <propstat>` +\n\t\t\t`      <prop>` +\n\t\t\t`        <Authors xmlns=\"http://ns.example.com/\"></Authors>` +\n\t\t\t`      </prop>` +\n\t\t\t`      <status>HTTP/1.1 424 Failed Dependency</status>` +\n\t\t\t`    </propstat>` +\n\t\t\t`    <propstat xmlns=\"DAV:\">` +\n\t\t\t`      <prop>` +\n\t\t\t`        <Copyright-Owner xmlns=\"http://ns.example.com/\"></Copyright-Owner>` +\n\t\t\t`      </prop>` +\n\t\t\t`      <status>HTTP/1.1 409 Conflict</status>` +\n\t\t\t`    </propstat>` +\n\t\t\t`  <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +\n\t\t\t`</response>` +\n\t\t\t`</multistatus>`,\n\t\twantCode: StatusMulti,\n\t}, {\n\t\tdesc: \"section 9.6.2 (lock-token-submitted)\",\n\t\tresponses: []response{{\n\t\t\tHref:   []string{\"http://example.com/foo\"},\n\t\t\tStatus: \"HTTP/1.1 423 Locked\",\n\t\t\tError: &xmlError{\n\t\t\t\tInnerXML: []byte(`<lock-token-submitted xmlns=\"DAV:\"/>`),\n\t\t\t},\n\t\t}},\n\t\twantXML: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"UTF-8\"?>` +\n\t\t\t`<multistatus xmlns=\"DAV:\">` +\n\t\t\t`  <response>` +\n\t\t\t`    <href>http://example.com/foo</href>` +\n\t\t\t`    <status>HTTP/1.1 423 Locked</status>` +\n\t\t\t`    <error><lock-token-submitted xmlns=\"DAV:\"/></error>` +\n\t\t\t`  </response>` +\n\t\t\t`</multistatus>`,\n\t\twantCode: StatusMulti,\n\t}, {\n\t\tdesc: \"section 9.1.3\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\"http://example.com/foo\"},\n\t\t\tPropstat: []propstat{{\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{Space: \"http://ns.example.com/boxschema/\", Local: \"bigbox\"},\n\t\t\t\t\tInnerXML: []byte(`` +\n\t\t\t\t\t\t`<BoxType xmlns=\"http://ns.example.com/boxschema/\">` +\n\t\t\t\t\t\t`Box type A` +\n\t\t\t\t\t\t`</BoxType>`),\n\t\t\t\t}, {\n\t\t\t\t\tXMLName: xml.Name{Space: \"http://ns.example.com/boxschema/\", Local: \"author\"},\n\t\t\t\t\tInnerXML: []byte(`` +\n\t\t\t\t\t\t`<Name xmlns=\"http://ns.example.com/boxschema/\">` +\n\t\t\t\t\t\t`J.J. Johnson` +\n\t\t\t\t\t\t`</Name>`),\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 200 OK\",\n\t\t\t}, {\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{Space: \"http://ns.example.com/boxschema/\", Local: \"DingALing\"},\n\t\t\t\t}, {\n\t\t\t\t\tXMLName: xml.Name{Space: \"http://ns.example.com/boxschema/\", Local: \"Random\"},\n\t\t\t\t}},\n\t\t\t\tStatus:              \"HTTP/1.1 403 Forbidden\",\n\t\t\t\tResponseDescription: \"The user does not have access to the DingALing property.\",\n\t\t\t}},\n\t\t}},\n\t\trespdesc: \"There has been an access violation error.\",\n\t\twantXML: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"UTF-8\"?>` +\n\t\t\t`<multistatus xmlns=\"DAV:\" xmlns:B=\"http://ns.example.com/boxschema/\">` +\n\t\t\t`  <response>` +\n\t\t\t`    <href>http://example.com/foo</href>` +\n\t\t\t`    <propstat>` +\n\t\t\t`      <prop>` +\n\t\t\t`        <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +\n\t\t\t`        <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +\n\t\t\t`      </prop>` +\n\t\t\t`      <status>HTTP/1.1 200 OK</status>` +\n\t\t\t`    </propstat>` +\n\t\t\t`    <propstat>` +\n\t\t\t`      <prop>` +\n\t\t\t`        <B:DingALing/>` +\n\t\t\t`        <B:Random/>` +\n\t\t\t`      </prop>` +\n\t\t\t`      <status>HTTP/1.1 403 Forbidden</status>` +\n\t\t\t`      <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +\n\t\t\t`    </propstat>` +\n\t\t\t`  </response>` +\n\t\t\t`  <responsedescription>There has been an access violation error.</responsedescription>` +\n\t\t\t`</multistatus>`,\n\t\twantCode: StatusMulti,\n\t}, {\n\t\tdesc: \"no response written\",\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc:     \"no response written (with description)\",\n\t\trespdesc: \"too bad\",\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc:        \"empty multistatus with header\",\n\t\twriteHeader: true,\n\t\twantXML:     `<multistatus xmlns=\"DAV:\"></multistatus>`,\n\t\twantCode:    StatusMulti,\n\t}, {\n\t\tdesc: \"bad: no href\",\n\t\tresponses: []response{{\n\t\t\tPropstat: []propstat{{\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\tSpace: \"http://example.com/\",\n\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 200 OK\",\n\t\t\t}},\n\t\t}},\n\t\twantErr: errInvalidResponse,\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc: \"bad: multiple hrefs and no status\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\"http://example.com/foo\", \"http://example.com/bar\"},\n\t\t}},\n\t\twantErr: errInvalidResponse,\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc: \"bad: one href and no propstat\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\"http://example.com/foo\"},\n\t\t}},\n\t\twantErr: errInvalidResponse,\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc: \"bad: status with one href and propstat\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\"http://example.com/foo\"},\n\t\t\tPropstat: []propstat{{\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\tSpace: \"http://example.com/\",\n\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 200 OK\",\n\t\t\t}},\n\t\t\tStatus: \"HTTP/1.1 200 OK\",\n\t\t}},\n\t\twantErr: errInvalidResponse,\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tdesc: \"bad: multiple hrefs and propstat\",\n\t\tresponses: []response{{\n\t\t\tHref: []string{\n\t\t\t\t\"http://example.com/foo\",\n\t\t\t\t\"http://example.com/bar\",\n\t\t\t},\n\t\t\tPropstat: []propstat{{\n\t\t\t\tProp: []Property{{\n\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\tSpace: \"http://example.com/\",\n\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStatus: \"HTTP/1.1 200 OK\",\n\t\t\t}},\n\t\t}},\n\t\twantErr: errInvalidResponse,\n\t\t// default of http.responseWriter\n\t\twantCode: http.StatusOK,\n\t}}\n\n\tn := xmlNormalizer{omitWhitespace: true}\nloop:\n\tfor _, tc := range testCases {\n\t\trec := httptest.NewRecorder()\n\t\tw := multistatusWriter{w: rec, responseDescription: tc.respdesc}\n\t\tif tc.writeHeader {\n\t\t\tif err := w.writeHeader(); err != nil {\n\t\t\t\tt.Errorf(\"%s: got writeHeader error %v, want nil\", tc.desc, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfor _, r := range tc.responses {\n\t\t\tif err := w.write(&r); err != nil {\n\t\t\t\tif err != tc.wantErr {\n\t\t\t\t\tt.Errorf(\"%s: got write error %v, want %v\",\n\t\t\t\t\t\ttc.desc, err, tc.wantErr)\n\t\t\t\t}\n\t\t\t\tcontinue loop\n\t\t\t}\n\t\t}\n\t\tif err := w.close(); err != tc.wantErr {\n\t\t\tt.Errorf(\"%s: got close error %v, want %v\",\n\t\t\t\ttc.desc, err, tc.wantErr)\n\t\t\tcontinue\n\t\t}\n\t\tif rec.Code != tc.wantCode {\n\t\t\tt.Errorf(\"%s: got HTTP status code %d, want %d\\n\",\n\t\t\t\ttc.desc, rec.Code, tc.wantCode)\n\t\t\tcontinue\n\t\t}\n\t\tgotXML := rec.Body.String()\n\t\teq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s: equalXML: %v\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\tif !eq {\n\t\t\tt.Errorf(\"%s: XML body\\ngot  %s\\nwant %s\", tc.desc, gotXML, tc.wantXML)\n\t\t}\n\t}\n}\n\nfunc TestReadProppatch(t *testing.T) {\n\tppStr := func(pps []Proppatch) string {\n\t\tvar outer []string\n\t\tfor _, pp := range pps {\n\t\t\tvar inner []string\n\t\t\tfor _, p := range pp.Props {\n\t\t\t\tinner = append(inner, fmt.Sprintf(\"{XMLName: %q, Lang: %q, InnerXML: %q}\",\n\t\t\t\t\tp.XMLName, p.Lang, p.InnerXML))\n\t\t\t}\n\t\t\touter = append(outer, fmt.Sprintf(\"{Remove: %t, Props: [%s]}\",\n\t\t\t\tpp.Remove, strings.Join(inner, \", \")))\n\t\t}\n\t\treturn \"[\" + strings.Join(outer, \", \") + \"]\"\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tinput      string\n\t\twantPP     []Proppatch\n\t\twantStatus int\n\t}{{\n\t\tdesc: \"proppatch: section 9.2 (with simple property value)\",\n\t\tinput: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"utf-8\" ?>` +\n\t\t\t`<D:propertyupdate xmlns:D=\"DAV:\"` +\n\t\t\t`                  xmlns:Z=\"http://ns.example.com/z/\">` +\n\t\t\t`    <D:set>` +\n\t\t\t`         <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +\n\t\t\t`    </D:set>` +\n\t\t\t`    <D:remove>` +\n\t\t\t`         <D:prop><Z:Copyright-Owner/></D:prop>` +\n\t\t\t`    </D:remove>` +\n\t\t\t`</D:propertyupdate>`,\n\t\twantPP: []Proppatch{{\n\t\t\tProps: []Property{{\n\t\t\t\txml.Name{Space: \"http://ns.example.com/z/\", Local: \"Authors\"},\n\t\t\t\t\"\",\n\t\t\t\t[]byte(`somevalue`),\n\t\t\t}},\n\t\t}, {\n\t\t\tRemove: true,\n\t\t\tProps: []Property{{\n\t\t\t\txml.Name{Space: \"http://ns.example.com/z/\", Local: \"Copyright-Owner\"},\n\t\t\t\t\"\",\n\t\t\t\tnil,\n\t\t\t}},\n\t\t}},\n\t}, {\n\t\tdesc: \"proppatch: lang attribute on prop\",\n\t\tinput: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"utf-8\" ?>` +\n\t\t\t`<D:propertyupdate xmlns:D=\"DAV:\">` +\n\t\t\t`    <D:set>` +\n\t\t\t`         <D:prop xml:lang=\"en\">` +\n\t\t\t`              <foo xmlns=\"http://example.com/ns\"/>` +\n\t\t\t`         </D:prop>` +\n\t\t\t`    </D:set>` +\n\t\t\t`</D:propertyupdate>`,\n\t\twantPP: []Proppatch{{\n\t\t\tProps: []Property{{\n\t\t\t\txml.Name{Space: \"http://example.com/ns\", Local: \"foo\"},\n\t\t\t\t\"en\",\n\t\t\t\tnil,\n\t\t\t}},\n\t\t}},\n\t}, {\n\t\tdesc: \"bad: remove with value\",\n\t\tinput: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"utf-8\" ?>` +\n\t\t\t`<D:propertyupdate xmlns:D=\"DAV:\"` +\n\t\t\t`                  xmlns:Z=\"http://ns.example.com/z/\">` +\n\t\t\t`    <D:remove>` +\n\t\t\t`         <D:prop>` +\n\t\t\t`              <Z:Authors>` +\n\t\t\t`              <Z:Author>Jim Whitehead</Z:Author>` +\n\t\t\t`              </Z:Authors>` +\n\t\t\t`         </D:prop>` +\n\t\t\t`    </D:remove>` +\n\t\t\t`</D:propertyupdate>`,\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"bad: empty propertyupdate\",\n\t\tinput: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"utf-8\" ?>` +\n\t\t\t`<D:propertyupdate xmlns:D=\"DAV:\"` +\n\t\t\t`</D:propertyupdate>`,\n\t\twantStatus: http.StatusBadRequest,\n\t}, {\n\t\tdesc: \"bad: empty prop\",\n\t\tinput: `` +\n\t\t\t`<?xml version=\"1.0\" encoding=\"utf-8\" ?>` +\n\t\t\t`<D:propertyupdate xmlns:D=\"DAV:\"` +\n\t\t\t`                  xmlns:Z=\"http://ns.example.com/z/\">` +\n\t\t\t`    <D:remove>` +\n\t\t\t`        <D:prop/>` +\n\t\t\t`    </D:remove>` +\n\t\t\t`</D:propertyupdate>`,\n\t\twantStatus: http.StatusBadRequest,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tpp, status, err := readProppatch(strings.NewReader(tc.input))\n\t\tif tc.wantStatus != 0 {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"%s: got nil error, want non-nil\", tc.desc)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if err != nil {\n\t\t\tt.Errorf(\"%s: %v\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\tif status != tc.wantStatus {\n\t\t\tt.Errorf(\"%s: got status %d, want %d\", tc.desc, status, tc.wantStatus)\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {\n\t\t\tt.Errorf(\"%s: proppatch\\ngot  %v\\nwant %v\", tc.desc, ppStr(pp), ppStr(tc.wantPP))\n\t\t}\n\t}\n}\n\nfunc TestUnmarshalXMLValue(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc    string\n\t\tinput   string\n\t\twantVal string\n\t}{{\n\t\tdesc:    \"simple char data\",\n\t\tinput:   \"<root>foo</root>\",\n\t\twantVal: \"foo\",\n\t}, {\n\t\tdesc:    \"empty element\",\n\t\tinput:   \"<root><foo/></root>\",\n\t\twantVal: \"<foo/>\",\n\t}, {\n\t\tdesc:    \"preserve namespace\",\n\t\tinput:   `<root><foo xmlns=\"bar\"/></root>`,\n\t\twantVal: `<foo xmlns=\"bar\"/>`,\n\t}, {\n\t\tdesc:    \"preserve root element namespace\",\n\t\tinput:   `<root xmlns:bar=\"bar\"><bar:foo/></root>`,\n\t\twantVal: `<foo xmlns=\"bar\"/>`,\n\t}, {\n\t\tdesc:    \"preserve whitespace\",\n\t\tinput:   \"<root>  \\t </root>\",\n\t\twantVal: \"  \\t \",\n\t}, {\n\t\tdesc:    \"preserve mixed content\",\n\t\tinput:   `<root xmlns=\"bar\">  <foo>a<bam xmlns=\"baz\"/> </foo> </root>`,\n\t\twantVal: `  <foo xmlns=\"bar\">a<bam xmlns=\"baz\"/> </foo> `,\n\t}, {\n\t\tdesc: \"section 9.2\",\n\t\tinput: `` +\n\t\t\t`<Z:Authors xmlns:Z=\"http://ns.example.com/z/\">` +\n\t\t\t`  <Z:Author>Jim Whitehead</Z:Author>` +\n\t\t\t`  <Z:Author>Roy Fielding</Z:Author>` +\n\t\t\t`</Z:Authors>`,\n\t\twantVal: `` +\n\t\t\t`  <Author xmlns=\"http://ns.example.com/z/\">Jim Whitehead</Author>` +\n\t\t\t`  <Author xmlns=\"http://ns.example.com/z/\">Roy Fielding</Author>`,\n\t}, {\n\t\tdesc: \"section 4.3.1 (mixed content)\",\n\t\tinput: `` +\n\t\t\t`<x:author ` +\n\t\t\t`    xmlns:x='http://example.com/ns' ` +\n\t\t\t`    xmlns:D=\"DAV:\">` +\n\t\t\t`  <x:name>Jane Doe</x:name>` +\n\t\t\t`  <!-- Jane's contact info -->` +\n\t\t\t`  <x:uri type='email'` +\n\t\t\t`         added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +\n\t\t\t`  <x:uri type='web'` +\n\t\t\t`         added='2005-11-27'>http://www.example.com</x:uri>` +\n\t\t\t`  <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +\n\t\t\t`    Jane has been working way <h:em>too</h:em> long on the` +\n\t\t\t`    long-awaited revision of <![CDATA[<RFC2518>]]>.` +\n\t\t\t`  </x:notes>` +\n\t\t\t`</x:author>`,\n\t\twantVal: `` +\n\t\t\t`  <name xmlns=\"http://example.com/ns\">Jane Doe</name>` +\n\t\t\t`  ` +\n\t\t\t`  <uri type='email'` +\n\t\t\t`       xmlns=\"http://example.com/ns\" ` +\n\t\t\t`       added='2005-11-26'>mailto:jane.doe@example.com</uri>` +\n\t\t\t`  <uri added='2005-11-27'` +\n\t\t\t`       type='web'` +\n\t\t\t`       xmlns=\"http://example.com/ns\">http://www.example.com</uri>` +\n\t\t\t`  <notes xmlns=\"http://example.com/ns\" ` +\n\t\t\t`         xmlns:h=\"http://www.w3.org/1999/xhtml\">` +\n\t\t\t`    Jane has been working way <h:em>too</h:em> long on the` +\n\t\t\t`    long-awaited revision of &lt;RFC2518&gt;.` +\n\t\t\t`  </notes>`,\n\t}}\n\n\tvar n xmlNormalizer\n\tfor _, tc := range testCases {\n\t\td := ixml.NewDecoder(strings.NewReader(tc.input))\n\t\tvar v xmlValue\n\t\tif err := d.Decode(&v); err != nil {\n\t\t\tt.Errorf(\"%s: got error %v, want nil\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\teq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s: equalXML: %v\", tc.desc, err)\n\t\t\tcontinue\n\t\t}\n\t\tif !eq {\n\t\t\tt.Errorf(\"%s:\\ngot  %s\\nwant %s\", tc.desc, string(v), tc.wantVal)\n\t\t}\n\t}\n}\n\n// xmlNormalizer normalizes XML.\ntype xmlNormalizer struct {\n\t// omitWhitespace instructs to ignore whitespace between element tags.\n\tomitWhitespace bool\n\t// omitComments instructs to ignore XML comments.\n\tomitComments bool\n}\n\n// normalize writes the normalized XML content of r to w. It applies the\n// following rules\n//\n//   - Rename namespace prefixes according to an internal heuristic.\n//   - Remove unnecessary namespace declarations.\n//   - Sort attributes in XML start elements in lexical order of their\n//     fully qualified name.\n//   - Remove XML directives and processing instructions.\n//   - Remove CDATA between XML tags that only contains whitespace, if\n//     instructed to do so.\n//   - Remove comments, if instructed to do so.\nfunc (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {\n\td := ixml.NewDecoder(r)\n\te := ixml.NewEncoder(w)\n\tfor {\n\t\tt, err := d.Token()\n\t\tif err != nil {\n\t\t\tif t == nil && err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tswitch val := t.(type) {\n\t\tcase ixml.Directive, ixml.ProcInst:\n\t\t\tcontinue\n\t\tcase ixml.Comment:\n\t\t\tif n.omitComments {\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase ixml.CharData:\n\t\t\tif n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase ixml.StartElement:\n\t\t\tstart, _ := ixml.CopyToken(val).(ixml.StartElement)\n\t\t\tattr := start.Attr[:0]\n\t\t\tfor _, a := range start.Attr {\n\t\t\t\tif a.Name.Space == \"xmlns\" || a.Name.Local == \"xmlns\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tattr = append(attr, a)\n\t\t\t}\n\t\t\tsort.Sort(byName(attr))\n\t\t\tstart.Attr = attr\n\t\t\tt = start\n\t\t}\n\t\terr = e.EncodeToken(t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn e.Flush()\n}\n\n// equalXML tests for equality of the normalized XML contents of a and b.\nfunc (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {\n\tvar buf bytes.Buffer\n\tif err := n.normalize(&buf, a); err != nil {\n\t\treturn false, err\n\t}\n\tnormA := buf.String()\n\tbuf.Reset()\n\tif err := n.normalize(&buf, b); err != nil {\n\t\treturn false, err\n\t}\n\tnormB := buf.String()\n\treturn normA == normB, nil\n}\n\ntype byName []ixml.Attr\n\nfunc (a byName) Len() int      { return len(a) }\nfunc (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a byName) Less(i, j int) bool {\n\tif a[i].Name.Space != a[j].Name.Space {\n\t\treturn a[i].Name.Space < a[j].Name.Space\n\t}\n\treturn a[i].Name.Local < a[j].Name.Local\n}\n"
  },
  {
    "path": "server/webdav.go",
    "content": "package server\n\nimport (\n\t\"crypto/subtle\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/model\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/op\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/setting\"\n\t\"github.com/OpenListTeam/OpenList/v4/internal/stream\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/common\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/middlewares\"\n\t\"github.com/OpenListTeam/OpenList/v4/server/webdav\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar handler *webdav.Handler\n\nfunc WebDav(dav *gin.RouterGroup) {\n\thandler = &webdav.Handler{\n\t\tPrefix:     path.Join(conf.URL.Path, \"/dav\"),\n\t\tLockSystem: webdav.NewMemLS(),\n\t\tLogger: func(request *http.Request, err error) {\n\t\t\tlog.Errorf(\"%s %s %+v\", request.Method, request.URL.Path, err)\n\t\t},\n\t}\n\tdav.Use(WebDAVAuth)\n\tuploadLimiter := middlewares.UploadRateLimiter(stream.ClientUploadLimit)\n\tdownloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit)\n\tdav.Any(\"/*path\", uploadLimiter, downloadLimiter, ServeWebDAV)\n\tdav.Any(\"\", uploadLimiter, downloadLimiter, ServeWebDAV)\n\tdav.Handle(\"PROPFIND\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"PROPFIND\", \"\", ServeWebDAV)\n\tdav.Handle(\"MKCOL\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"LOCK\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"UNLOCK\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"PROPPATCH\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"COPY\", \"/*path\", ServeWebDAV)\n\tdav.Handle(\"MOVE\", \"/*path\", ServeWebDAV)\n}\n\nfunc ServeWebDAV(c *gin.Context) {\n\thandler.ServeHTTP(c.Writer, c.Request)\n}\n\nfunc WebDAVAuth(c *gin.Context) {\n\t// check count of login\n\tip := c.ClientIP()\n\tguest, _ := op.GetGuest()\n\tcount, cok := model.LoginCache.Get(ip)\n\tif cok && count >= model.DefaultMaxAuthRetries {\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tc.Status(http.StatusTooManyRequests)\n\t\tc.Abort()\n\t\tmodel.LoginCache.Expire(ip, model.DefaultLockDuration)\n\t\treturn\n\t}\n\tusername, password, ok := c.Request.BasicAuth()\n\tif !ok {\n\t\tbt := c.GetHeader(\"Authorization\")\n\t\tlog.Debugf(\"[webdav auth] token: %s\", bt)\n\t\tif strings.HasPrefix(bt, \"Bearer\") {\n\t\t\tbt = strings.TrimPrefix(bt, \"Bearer \")\n\t\t\ttoken := setting.GetStr(conf.Token)\n\t\t\tif token != \"\" && subtle.ConstantTimeCompare([]byte(bt), []byte(token)) == 1 {\n\t\t\t\tadmin, err := op.GetAdmin()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"[webdav auth] failed get admin user: %+v\", err)\n\t\t\t\t\tc.Status(http.StatusInternalServerError)\n\t\t\t\t\tc.Abort()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcommon.GinWithValue(c, conf.UserKey, admin)\n\t\t\t\tc.Next()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tc.Writer.Header()[\"WWW-Authenticate\"] = []string{`Basic realm=\"openlist\"`}\n\t\tc.Status(http.StatusUnauthorized)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tuser, ok := tryLogin(username, password)\n\tif !ok {\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tmodel.LoginCache.Set(ip, count+1)\n\t\tc.Status(http.StatusUnauthorized)\n\t\tc.Abort()\n\t\treturn\n\t}\n\t// at least auth is successful till here\n\tmodel.LoginCache.Del(ip)\n\tif user.Disabled || !user.CanWebdavRead() {\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tcommon.GinWithValue(c, conf.UserKey, guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif (c.Request.Method == \"PUT\" || c.Request.Method == \"MKCOL\") && (!user.CanWebdavManage() || !user.CanWrite()) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"MOVE\" && (!user.CanWebdavManage() || (!user.CanMove() && !user.CanRename())) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"COPY\" && (!user.CanWebdavManage() || !user.CanCopy()) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"DELETE\" && (!user.CanWebdavManage() || !user.CanRemove()) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"PROPPATCH\" && !user.CanWebdavManage() {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tcommon.GinWithValue(c, conf.UserKey, user)\n\tc.Next()\n}\n\nfunc tryLogin(username, password string) (*model.User, bool) {\n\tuser, err := op.GetUserByName(username)\n\tif err == nil {\n\t\terr = user.ValidateRawPassword(password)\n\t\tif err != nil && setting.GetBool(conf.LdapLoginEnabled) && user.AllowLdap {\n\t\t\terr = common.HandleLdapLogin(username, password)\n\t\t}\n\t} else if setting.GetBool(conf.LdapLoginEnabled) && model.CanWebdavRead(int32(setting.GetInt(conf.LdapDefaultPermission, 0))) {\n\t\tuser, err = tryLdapLoginAndRegister(username, password)\n\t}\n\treturn user, err == nil\n}\n"
  },
  {
    "path": "wrapper/zcc-arm64",
    "content": "#!/bin/sh\nzig cc -target aarch64-windows-gnu $@\n"
  },
  {
    "path": "wrapper/zcc-win7",
    "content": "#!/bin/sh\nzig cc -target x86_64-windows-gnu $@"
  },
  {
    "path": "wrapper/zcc-win7-386",
    "content": "#!/bin/sh\nzig cc -target x86-windows-gnu $@"
  },
  {
    "path": "wrapper/zcxx-arm64",
    "content": "#!/bin/sh\nzig c++ -target aarch64-windows-gnu $@\n"
  },
  {
    "path": "wrapper/zcxx-win7",
    "content": "#!/bin/sh\nzig c++ -target x86_64-windows-gnu $@"
  },
  {
    "path": "wrapper/zcxx-win7-386",
    "content": "#!/bin/sh\nzig c++ -target x86-windows-gnu $@"
  }
]