[
  {
    "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: xhofe # 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: ['https://alistgo.com/guide/sponsor.html']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"Bug report\"\ndescription: Bug report\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report, please **confirm that your issue is not a duplicate issue and not because of your operation or version issues**\n        感谢您花时间填写此错误报告，请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**\n\n  - type: checkboxes\n    attributes:\n      label: Please make sure of the following things\n      description: |\n        You must check all the following, otherwise your issue may be closed directly. Or you can go to the [discussions](https://github.com/alist-org/alist/discussions)\n        您必须勾选以下所有内容，否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/alist-org/alist/discussions)\n      options:\n        - label: |\n            I have read the [documentation](https://alistgo.com).\n            我已经阅读了[文档](https://alistgo.com)。\n        - label: |\n            I'm sure there are no duplicate issues or discussions.\n            我确定没有重复的issue或讨论。\n        - label: |\n            I'm sure it's due to `AList` and not something else(such as [Network](https://alistgo.com/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).\n            我确定是`AList`的问题，而不是其他原因（例如[网络](https://alistgo.com/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)，`依赖`或`操作`）。\n        - label: |\n            I'm sure this issue is not fixed in the latest version.\n            我确定这个问题在最新版本中没有被修复。\n\n  - type: input\n    id: version\n    attributes:\n      label: AList Version / AList 版本\n      description: |\n        What version of our software are you running? Do not use `latest` or `master` as an answer.\n        您使用的是哪个版本的软件？请不要使用`latest`或`master`作为答案。\n      placeholder: v3.xx.xx\n    validations:\n      required: true\n  - type: input\n    id: driver\n    attributes:\n      label: Driver used / 使用的存储驱动\n      description: |\n        What storage driver are you using?\n        您使用的是哪个存储驱动？\n      placeholder: \"for example: Onedrive\"\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug / 问题描述\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Reproduction / 复现链接\n      description: |\n        Please provide a link to a repo that can reproduce the problem you ran into. Please be aware that your issue may be closed directly if you don't provide it.\n        请提供能复现此问题的链接，请知悉如果不提供它你的issue可能会被直接关闭。\n    validations:\n      required: true\n  - type: textarea\n    id: config\n    attributes:\n      label: Config / 配置\n      description: |\n        Please provide the configuration file of your `AList` application and take a screenshot of the relevant storage configuration. (hide privacy field)\n        请提供您的`AList`应用的配置文件，并截图相关存储配置。(隐藏隐私字段)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs / 日志\n      description: |\n        Please copy and paste any relevant log output.\n        请复制粘贴错误日志，或者截图\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Discussions\n    url: https://github.com/alist-org/alist/discussions\n    about: Use GitHub discussions for message-board style questions and discussions."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"Feature request\"\ndescription: Feature request\nlabels: [enhancement]\nbody:\n  - type: checkboxes\n    attributes:\n      label: Please make sure of the following things\n      description: You may select more than one, even select all.\n      options:\n        - label: I have read the [documentation](https://alistgo.com).\n        - label: I'm sure there are no duplicate issues or discussions.\n        - label: I'm sure this feature is not implemented.\n        - label: I'm sure it's a reasonable and popular requirement.\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Description of the feature / 需求描述\n    validations:\n      required: true\n  - type: textarea\n    id: suggested-solution\n    attributes:\n      label: Suggested solution / 实现思路\n      description: |\n        Solutions to achieve this requirement.\n        实现此需求的解决思路。\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional context / 附件\n      description: |\n        Any other context or screenshots about the feature request here, or information you find helpful.\n        相关的任何其他上下文或截图，或者你觉得有帮助的信息"
  },
  {
    "path": ".github/config.yml",
    "content": "# Configuration for welcome - https://github.com/behaviorbot/welcome\n\n# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome\n\n# Comment to be posted to on first time issues\nnewIssueWelcomeComment: >\n  Thanks for opening your first issue here! Be sure to follow the issue template!\n\n# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome\n\n# Comment to be posted to on PRs from first time contributors in your repository\nnewPRWelcomeComment: >\n  Thanks for opening this pull request! Please check out our contributing guidelines.\n\n# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge\n\n# Comment to be posted to on pull requests merged by a first time user\nfirstPRMergeComment: >\n  Congrats on merging your first pull request! We here at behavior bot are proud of you! \n\n# It is recommend to include as many gifs and emojis as possible"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 44\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 20\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - accepted\n  - security\n  - working\n  - pr-welcome\n# Label to use when marking an issue as stale\nstaleLabel: stale\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: >\n  This issue was closed due to inactive more than 52 days. You can reopen or\n  recreate it if you think it should continue. Thank you for your contributions again.\n"
  },
  {
    "path": ".github/workflows/auto_lang.yml",
    "content": "name: auto_lang\n\non:\n  push:\n    branches:\n      - 'main'\n    paths:\n      - 'drivers/**'\n      - 'internal/bootstrap/data/setting.go'\n      - 'internal/conf/const.go'\n      - 'cmd/lang.go'\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  auto_lang:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: auto generate lang.json\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Setup go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout alist\n        uses: actions/checkout@v4\n        with:\n          path: alist\n\n      - name: Checkout alist-web\n        uses: actions/checkout@v4\n        with:\n          repository: 'alist-org/alist-web'\n          ref: main\n          persist-credentials: false\n          fetch-depth: 0\n          path: alist-web\n\n      - name: Generate lang\n        run: |\n          cd alist\n          go run ./main.go lang\n          cd ..\n\n      - name: Copy lang file\n        run: |\n          cp -f ./alist/lang/*.json ./alist-web/src/lang/en/ 2>/dev/null || :\n\n      - name: Commit git\n        run: |\n          cd alist-web\n          git add .\n          git config --local user.email \"bot@nn.ci\"\n          git config --local user.name \"IlaBot\"\n          git commit -m \"chore: auto update i18n file\" -a 2>/dev/null || :\n          cd ..\n\n      - name: Push lang files\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.MY_TOKEN }}\n          branch: main\n          directory: alist-web\n          repository: alist-org/alist-web\n"
  },
  {
    "path": ".github/workflows/beta_release.yml",
    "content": "name: beta release\n\non:\n  push:\n    branches: [ 'main' ]\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    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Beta Release Changelog\n    runs-on: ${{ matrix.platform }}\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#          npx changelogen@latest --output CHANGELOG.md\n\n      - name: Upload assets\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  release:\n    needs:\n      - changelog\n    strategy:\n      matrix:\n        include:\n          - target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo\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: '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.22'\n\n      - name: Setup web\n        run: bash build.sh dev web\n\n      - name: Build\n        uses: go-cross/cgo-actions@v1\n        with:\n          targets: ${{ matrix.target }}\n          musl-target-format: $os-$musl-$arch\n          out-dir: build\n          x-flags: |\n            github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at\n            github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe\n            github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit\n            github.com/alist-org/alist/v3/internal/conf.Version=$tag\n            github.com/alist-org/alist/v3/internal/conf.WebVersion=dev\n\n      - name: Compress\n        run: |\n          bash build.sh zip ${{ matrix.hash }}\n          \n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n          prerelease: true\n          tag_name: beta\n          \n  desktop:\n    needs:\n      - release\n    name: Beta Release Desktop\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v4\n        with:\n          repository: AlistGo/desktop-release\n          ref: main\n          persist-credentials: false\n          fetch-depth: 0\n\n      - name: Commit\n        run: |\n          git config --local user.email \"bot@nn.ci\"\n          git config --local user.name \"IlaBot\"\n          git commit --allow-empty -m \"Trigger build for ${{ github.sha }}\"\n\n      - name: Push commit\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.MY_TOKEN }}\n          branch: main\n          repository: AlistGo/desktop-release"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n  push:\n    branches: [ 'main' ]\n  pull_request:\n    branches: [ 'main' ]\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        platform: [ubuntu-latest]\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\n    runs-on: ${{ matrix.platform }}\n    env:\n      GOPROXY: https://proxy.golang.org,direct\n    steps:\n\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.22'\n\n      - name: Setup web\n        run: bash build.sh dev web\n\n      - name: Build\n        uses: go-cross/cgo-actions@v1\n        with:\n          targets: ${{ matrix.target }}\n          musl-target-format: $os-$musl-$arch\n          out-dir: build\n          x-flags: |\n            github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at\n            github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe\n            github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit\n            github.com/alist-org/alist/v3/internal/conf.Version=$tag\n            github.com/alist-org/alist/v3/internal/conf.WebVersion=dev\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: alist_${{ env.SHA }}_${{ matrix.target }}\n          path: build/*"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: auto changelog\n\non:\n  push:\n    tags:\n      - 'v*'\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.MY_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/issue_close_question.yml",
    "content": "name: Close need info\n\non:\n  schedule:\n    - cron: \"0 0 */1 * *\"\n  workflow_dispatch:\n\njobs:\n  close-need-info:\n    runs-on: ubuntu-latest\n    steps:\n      - name: close-issues\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issues'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          labels: 'question'\n          inactive-day: 3\n          close-reason: 'not_planned'\n          body: |\n            Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 3 days.\n            你好 @${{ github.event.issue.user.login }}，此issue因超过3天未回复被关闭。"
  },
  {
    "path": ".github/workflows/issue_close_stale.yml",
    "content": "name: Close inactive\n\non:\n  schedule:\n    - cron: \"0 0 */7 * *\"\n  workflow_dispatch:\n\njobs:\n  close-inactive:\n    runs-on: ubuntu-latest\n    steps:\n      - name: close-issues\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issues'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          labels: 'stale'\n          inactive-day: 8\n          close-reason: 'not_planned'\n          body: |\n            Hello @${{ github.event.issue.user.login }}, this issue was closed due to inactive more than 52 days. You can reopen or recreate it if you think it should continue. Thank you for your contributions again."
  },
  {
    "path": ".github/workflows/issue_duplicate.yml",
    "content": "name: Issue Duplicate\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  create-comment:\n    runs-on: ubuntu-latest\n    if: github.event.label.name == 'duplicate'\n    steps:\n      - name: Create comment\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.\n            你好 @${{ github.event.issue.user.login }}，你的issue是重复的，将被关闭。\n      - name: Close issue\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issue'\n          token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/issue_invalid.yml",
    "content": "name: Issue Invalid\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  create-comment:\n    runs-on: ubuntu-latest\n    if: github.event.label.name == 'invalid'\n    steps:\n      - name: Create comment\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}, your issue is invalid and will be closed.\n            你好 @${{ github.event.issue.user.login }}，你的issue无效，将被关闭。\n      - name: Close issue\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issue'\n          token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/issue_on_close.yml",
    "content": "name: Remove working label when issue closed\n\non:\n  issues:\n    types: [closed]\n\njobs:\n  rm-working:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Remove working label\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'remove-labels'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          labels: 'working,pr-welcome'\n"
  },
  {
    "path": ".github/workflows/issue_question.yml",
    "content": "name: Issue Question\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  create-comment:\n    runs-on: ubuntu-latest\n    if: github.event.label.name == 'question'\n    steps:\n      - name: Create comment\n        uses: actions-cool/issues-helper@v3.6.0\n        with:\n          actions: 'create-comment'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 3 days.\n            你好 @${{ github.event.issue.user.login }}，请按照issue模板填写, 并详细说明问题/日志记录/复现步骤/复现链接/实现思路或提供更多信息等, 3天内未回复issue自动关闭。"
  },
  {
    "path": ".github/workflows/issue_similarity.yml",
    "content": "name: Issues Similarity Analysis\n\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  similarity-analysis:\n    runs-on: ubuntu-latest\n    steps:\n      - name: analysis\n        uses: actions-cool/issues-similarity-analysis@v1\n        with:\n          filter-threshold: 0.5\n          comment-title: '### See'\n          comment-body: '${index}. ${similarity} #${number}'\n          show-footer: false\n          show-mentioned: true\n          since-days: 730"
  },
  {
    "path": ".github/workflows/issue_translate.yml",
    "content": "name: Translation Helper\n\non:\n  pull_request_target:\n    types: [opened]\n  issues:\n    types: [opened]\n\njobs:\n  translate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions-cool/translation-helper@v1.2.0"
  },
  {
    "path": ".github/workflows/issue_wontfix.yml",
    "content": "name: Issue Wontfix\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  lock-issue:\n    runs-on: ubuntu-latest\n    if: github.event.label.name == 'wontfix'\n    steps:\n      - name: Create comment\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.\n            你好 @${{ github.event.issue.user.login }}，这不会被处理，将被关闭。\n      - name: Close issue\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issue'\n          token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  release:\n    types: [ published ]\n\njobs:\n  release:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Release\n    runs-on: ${{ matrix.platform }}\n    steps:\n\n      - name: Free Disk Space (Ubuntu)\n        uses: jlumbroso/free-disk-space@main\n        with:\n          # this might remove tools that are actually needed,\n          # if set to \"true\" but frees about 6 GB\n          tool-cache: false\n          \n          # all of these default to true, but feel free to set to\n          # \"false\" if necessary for your workflow\n          android: true\n          dotnet: true\n          haskell: true\n          large-packages: true\n          docker-images: true\n          swap-storage: true\n\n      - name: Prerelease\n        uses: irongut/EditRelease@v1.2.0\n        with:\n          token: ${{ secrets.MY_TOKEN }}\n          id: ${{ github.event.release.id }}\n          prerelease: true\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install dependencies\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\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n          prerelease: false\n\n  release_desktop:\n    needs: release\n    name: Release desktop\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v4\n        with:\n          repository: AlistGo/desktop-release\n          ref: main\n          persist-credentials: false\n          fetch-depth: 0\n\n      - name: Add tag\n        run: |\n          git config --local user.email \"bot@nn.ci\"\n          git config --local user.name \"IlaBot\"\n          version=$(wget -qO- -t1 -T2 \"https://api.github.com/repos/alist-org/alist/releases/latest\" | grep \"tag_name\" | head -n 1 | awk -F \":\" '{print $2}' | sed 's/\\\"//g;s/,//g;s/ //g')\n          git tag -a $version -m \"release $version\"\n\n      - name: Push tags\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.MY_TOKEN }}\n          branch: main\n          repository: AlistGo/desktop-release"
  },
  {
    "path": ".github/workflows/release_android.yml",
    "content": "name: release_android\n\non:\n  release:\n    types: [ published ]\n\njobs:\n  release_android:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Release\n    runs-on: ${{ matrix.platform }}\n    steps:\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Build\n        run: |\n          bash build.sh release android\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n"
  },
  {
    "path": ".github/workflows/release_docker.yml",
    "content": "name: release_docker\n\non:\n  push:\n    tags:\n      - 'v*'\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  REGISTRY: 'xhofe/alist'\n  REGISTRY_USERNAME: 'xhofe'\n  REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}\n  GITHUB_CR_REPO: ghcr.io/${{ github.repository }}\n  ARTIFACT_NAME: 'binaries_docker_release'\n  RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'\n  IMAGE_PUSH: ${{ github.event_name == 'push' }}\n  IMAGE_IS_PROD: ${{ github.ref_type == 'tag' }}\n  IMAGE_TAGS_BETA: |\n    type=schedule\n    type=ref,event=branch\n    type=ref,event=tag\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\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: 'stable'\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\n      - name: Build go binary (beta)\n        if: env.IMAGE_IS_PROD != 'true'\n        run: bash build.sh beta docker-multiplatform\n\n      - name: Build go binary (release)\n        if: env.IMAGE_IS_PROD == 'true'\n        run: bash build.sh release docker-multiplatform\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\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        image: [\"latest\", \"ffmpeg\", \"aria2\", \"aio\"]\n        include:\n          - image: \"latest\"\n            build_arg: \"\"\n            tag_favor: \"\"\n          - image: \"ffmpeg\"\n            build_arg: INSTALL_FFMPEG=true\n            tag_favor: \"suffix=-ffmpeg,onlatest=true\"\n          - image: \"aria2\"\n            build_arg: INSTALL_ARIA2=true\n            tag_favor: \"suffix=-aria2,onlatest=true\"\n          - image: \"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 DockerHub\n        if: env.IMAGE_PUSH == 'true'\n        uses: docker/login-action@v3\n        with:\n          logout: true\n          username: ${{ env.REGISTRY_USERNAME }}\n          password: ${{ env.REGISTRY_PASSWORD }}\n\n      - name: Login to GHCR\n        uses: docker/login-action@v3\n        with:\n          logout: true\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ env.REGISTRY }}\n            ${{ env.GITHUB_CR_REPO }}\n          tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }}\n          flavor: |\n            ${{ env.IMAGE_IS_PROD == 'true' && '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: ${{ matrix.build_arg }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: ${{ env.RELEASE_PLATFORMS }}"
  },
  {
    "path": ".github/workflows/release_freebsd.yml",
    "content": "name: release_freebsd\n\non:\n  release:\n    types: [ published ]\n\njobs:\n  release_freebsd:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Release\n    runs-on: ${{ matrix.platform }}\n    steps:\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Build\n        run: |\n          bash build.sh release freebsd\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n"
  },
  {
    "path": ".github/workflows/release_linux_musl.yml",
    "content": "name: release_linux_musl\n\non:\n  release:\n    types: [ published ]\n\njobs:\n  release_linux_musl:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Release\n    runs-on: ${{ matrix.platform }}\n    steps:\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Build\n        run: |\n          bash build.sh release linux_musl\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n"
  },
  {
    "path": ".github/workflows/release_linux_musl_arm.yml",
    "content": "name: release_linux_musl_arm\n\non:\n  release:\n    types: [ published ]\n\njobs:\n  release_linux_musl_arm:\n    strategy:\n      matrix:\n        platform: [ ubuntu-latest ]\n        go-version: [ '1.21' ]\n    name: Release\n    runs-on: ${{ matrix.platform }}\n    steps:\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Build\n        run: |\n          bash build.sh release linux_musl_arm\n\n      - name: Upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/compress/*\n"
  },
  {
    "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\ni@nn.ci.\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`alist` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).\n\nPrerequisites:\n\n- [git](https://git-scm.com)\n- [Go 1.20+](https://golang.org/doc/install)\n- [gcc](https://gcc.gnu.org/)\n- [nodejs](https://nodejs.org/)\n\nClone `alist` and `alist-web` anywhere:\n\n```shell\n$ git clone https://github.com/alist-org/alist.git\n$ git clone --recurse-submodules https://github.com/alist-org/alist-web.git\n```\nYou should switch to the `main` branch for development.\n\n## Preview your change\n### backend\n```shell\n$ go run main.go\n```\n### frontend\n```shell\n$ pnpm dev\n```\n\n## Add a new driver\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\n### Commit Message Format\nEach commit message consists of a **header**, a **body** and a **footer**.  The header has a special\nformat that includes a **type**, a **scope** and a **subject**:\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\nThe **header** is mandatory and the **scope** of the header is optional.\n\nAny line of the commit message cannot be longer than 100 characters! This allows the message to be easier\nto read on GitHub as well as in various git tools.\n\n### Revert\nIf the commit reverts a previous commit, it should begin with `revert: `, followed by the header\nof the reverted commit.\nIn the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit\nbeing reverted.\n\n### Type\nMust be one of the following:\n\n* **feat**: A new feature\n* **fix**: A bug fix\n* **docs**: Documentation only changes\n* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing\n  semi-colons, etc)\n* **refactor**: A code change that neither fixes a bug nor adds a feature\n* **perf**: A code change that improves performance\n* **test**: Adding missing or correcting existing tests\n* **build**: Affects project builds or dependency modifications\n* **revert**: Restore the previous commit\n* **ci**: Continuous integration of related file modifications\n* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation\n  generation\n* **release**: Release a new version\n\n### Scope\nThe scope could be anything specifying place of the commit change. For example `$location`,\n`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...\n\nYou can use `*` when the change affects more than a single scope.\n\n### Subject\nThe subject contains succinct description of the change:\n\n* use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n* don't capitalize first letter\n* no dot (.) at the end\n\n### Body\nJust as in the **subject**, use the imperative, present tense: \"change\" not \"changed\" nor \"changes\".\nThe body should include the motivation for the change and contrast this with previous behavior.\n\n### Footer\nThe footer should contain any information about **Breaking Changes** and is also the place to\n[reference GitHub issues that this commit closes](https://help.github.com/articles/closing-issues-via-commit-messages/).\n\n**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines.\nThe rest of the commit message is then used for this.\n\n## Submit a pull request\n\nPush your branch to your `alist` fork and open a pull request against the\n`main` branch.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:edge as builder\nLABEL stage=go-builder\nWORKDIR /app/\nRUN apk add --no-cache bash curl gcc git go musl-dev\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY ./ ./\nRUN bash build.sh release docker\n\nFROM alpine:edge\n\nARG INSTALL_FFMPEG=false\nARG INSTALL_ARIA2=false\nLABEL MAINTAINER=\"i@nn.ci\"\n\nWORKDIR /opt/alist/\n\nRUN apk update && \\\n    apk upgrade --no-cache && \\\n    apk add --no-cache bash ca-certificates su-exec tzdata; \\\n    [ \"$INSTALL_FFMPEG\" = \"true\" ] && apk add --no-cache ffmpeg; \\\n    [ \"$INSTALL_ARIA2\" = \"true\" ] && apk add --no-cache curl aria2 && \\\n        mkdir -p /opt/aria2/.aria2 && \\\n        wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \\\n        tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \\\n        sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \\\n        sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \\\n        touch /opt/aria2/.aria2/aria2.session && \\\n        /opt/aria2/.aria2/tracker.sh ; \\\n    rm -rf /var/cache/apk/*\n\nCOPY --chmod=755 --from=builder /app/bin/alist ./\nCOPY --chmod=755 entrypoint.sh /entrypoint.sh\nRUN /entrypoint.sh version\n\nENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}\nVOLUME /opt/alist/data/\nEXPOSE 5244 5245\nCMD [ \"/entrypoint.sh\" ]"
  },
  {
    "path": "Dockerfile.ci",
    "content": "FROM alpine:3.20.7\n\nARG TARGETPLATFORM\nARG INSTALL_FFMPEG=false\nARG INSTALL_ARIA2=false\nLABEL MAINTAINER=\"i@nn.ci\"\n\nWORKDIR /opt/alist/\n\nRUN apk update && \\\n    apk upgrade --no-cache && \\\n    apk add --no-cache bash ca-certificates su-exec tzdata; \\\n    [ \"$INSTALL_FFMPEG\" = \"true\" ] && apk add --no-cache ffmpeg; \\\n    [ \"$INSTALL_ARIA2\" = \"true\" ] && apk add --no-cache curl aria2 && \\\n        mkdir -p /opt/aria2/.aria2 && \\\n        wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \\\n        tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \\\n        sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \\\n        sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \\\n        sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \\\n        touch /opt/aria2/.aria2/aria2.session && \\\n        /opt/aria2/.aria2/tracker.sh ; \\\n    rm -rf /var/cache/apk/*\n\nCOPY --chmod=755 /build/${TARGETPLATFORM}/alist ./\nCOPY --chmod=755 entrypoint.sh /entrypoint.sh\nRUN /entrypoint.sh version\n\nENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}\nVOLUME /opt/alist/data/\nEXPOSE 5244 5245\nCMD [ \"/entrypoint.sh\" ]\n"
  },
  {
    "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  <a href=\"https://alistgo.com\"><img width=\"100px\" alt=\"logo\" src=\"https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg\"/></a>\n  <p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>\n<div>\n  <a href=\"https://goreportcard.com/report/github.com/alist-org/alist/v3\">\n    <img src=\"https://goreportcard.com/badge/github.com/alist-org/alist/v3\" alt=\"latest version\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/Xhofe/alist\" alt=\"License\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/actions?query=workflow%3ABuild\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main\" alt=\"Build status\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/release/Xhofe/alist\" alt=\"latest version\" />\n  </a>\n  <a title=\"Crowdin\" target=\"_blank\" href=\"https://crwd.in/alist\">\n    <img src=\"https://badges.crowdin.net/alist/localized.svg\">\n  </a>\n</div>\n<div>\n  <a href=\"https://github.com/alist-org/alist/discussions\">\n    <img src=\"https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://discord.gg/F4ymsH4xv2\">\n    <img src=\"https://img.shields.io/discord/1018870125102895134?logo=discord\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://hub.docker.com/r/xhofe/alist\">\n    <img src=\"https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://alistgo.com/guide/sponsor.html\">\n    <img src=\"https://img.shields.io/badge/%24-sponsor-F87171.svg\" alt=\"sponsor\" />\n  </a>\n</div>\n</div>\n\n---\n\nEnglish | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)\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.office.com/), [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\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(Support OneDrive/SharePoint without API)\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] [Aliyundrive share](https://www.alipan.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\n    - [x] [115](https://115.com/)\n    - [X] Cloudreve\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] 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 (see https://alistgo.com/guide/webdav.html for details)\n- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)\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<https://alistgo.com/>\n\n## API Documentation (via Apifox):\n\n<https://alist-public.apifox.cn/>\n\n## Demo\n\n<https://al.nn.ci>\n\n## Discussion\n\nPlease go to our [discussion forum](https://github.com/alist-org/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**\n\n## Sponsor\n\nAList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support:\nhttps://alistgo.com/guide/sponsor.html\n\n### Special sponsors\n\n- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.\n\n## Contributors\n\nThanks goes to these wonderful people:\n\n[![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)\n\n## License\n\nThe `AList` is open-source software licensed under the AGPL-3.0 license.\n\n## Disclaimer\n- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning Golang. Please abide by relevant laws and regulations when using it, and do not abuse it;\n- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;\n- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;\n- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;\n- If there is any infringement, please contact me by [email](mailto:i@nn.ci), and it will be dealt with in time.\n\n---\n\n> [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)\n"
  },
  {
    "path": "README_cn.md",
    "content": "<div align=\"center\">\n  <a href=\"https://alistgo.com\"><img width=\"100px\" alt=\"logo\" src=\"https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg\"/></a>\n  <p><em>🗂一个支持多存储的文件列表程序，使用 Gin 和 Solidjs。</em></p>\n<div>\n  <a href=\"https://goreportcard.com/report/github.com/alist-org/alist/v3\">\n    <img src=\"https://goreportcard.com/badge/github.com/alist-org/alist/v3\" alt=\"latest version\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/Xhofe/alist\" alt=\"License\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/actions?query=workflow%3ABuild\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main\" alt=\"Build status\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/release/Xhofe/alist\" alt=\"latest version\" />\n  </a>\n  <a title=\"Crowdin\" target=\"_blank\" href=\"https://crwd.in/alist\">\n    <img src=\"https://badges.crowdin.net/alist/localized.svg\">\n  </a>\n</div>\n<div>\n  <a href=\"https://github.com/alist-org/alist/discussions\">\n    <img src=\"https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://discord.gg/F4ymsH4xv2\">\n    <img src=\"https://img.shields.io/discord/1018870125102895134?logo=discord\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://hub.docker.com/r/xhofe/alist\">\n    <img src=\"https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://alistgo.com/zh/guide/sponsor.html\">\n    <img src=\"https://img.shields.io/badge/%24-sponsor-F87171.svg\" alt=\"sponsor\" />\n  </a>\n</div>\n</div>\n\n---\n\n[English](./README.md) | 中文 | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)\n\n## 功能\n\n- [x] 多种存储\n    - [x] 本地存储\n    - [x] [阿里云盘](https://www.alipan.com/)\n    - [x] OneDrive / Sharepoint（[国际版](https://www.office.com/), [世纪互联](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\n    - [x] [PikPak](https://www.mypikpak.com/)\n    - [x] [S3](https://aws.amazon.com/cn/s3/)\n    - [x] [Seafile](https://seafile.com/)\n    - [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)\n    - [x] WebDav(支持无API的OneDrive/SharePoint)\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] [Yandex.Disk](https://disk.yandex.com/)\n    - [x] [百度网盘](http://pan.baidu.com/)\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] [阿里云盘分享](https://www.alipan.com/)\n    - [x] [谷歌相册](https://photos.google.com/)\n    - [x] [Mega.nz](https://mega.nz)\n    - [x] [一刻相册](https://photo.baidu.com/)\n    - [x] SMB\n    - [x] [115](https://115.com/)\n    - [X] Cloudreve\n    - [x] [Dropbox](https://www.dropbox.com/)\n    - [x] [飞机盘](https://www.feijipan.com/)\n    - [x] [多吉云](https://www.dogecloud.com/product/oss)\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 (具体见 https://alistgo.com/zh/guide/webdav.html)\n- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)\n- [x] Cloudflare workers 中转\n- [x] 文件/文件夹打包下载\n- [x] 网页上传(可以允许访客上传)，删除，新建文件夹，重命名，移动，复制\n- [x] 离线下载\n- [x] 跨存储复制文件\n- [x] 单线程下载/串流的多线程下载加速\n\n## 文档\n\n<https://alistgo.com/zh/>\n\n## API 文档（通过 Apifox 提供）\n\n<https://alist-public.apifox.cn/>\n\n## Demo\n\n<https://al.nn.ci>\n\n## 讨论\n\n一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) ，**issue仅针对错误报告和功能请求。**\n\n## 赞助\n\nAList 是一个开源软件，如果你碰巧喜欢这个项目，并希望我继续下去，请考虑赞助我或提供一个单一的捐款！感谢所有的爱和支持：https://alistgo.com/zh/guide/sponsor.html\n\n### 特别赞助\n\n- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器，iPhone，iPad，Mac，Apple TV全平台支持。\n\n## 贡献者\n\nThanks goes to these wonderful people:\n\n[![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)\n\n## 许可\n\n`AList` 是在 AGPL-3.0 许可下许可的开源软件。\n\n## 免责声明\n- 本程序为免费开源项目，旨在分享网盘文件，方便下载以及学习golang，使用时请遵守相关法律法规，请勿滥用；\n- 本程序通过调用官方sdk/接口实现，无破坏官方接口行为；\n- 本程序仅做302重定向/流量转发，不拦截、存储、篡改任何用户数据；\n- 在使用本程序之前，你应了解并承担相应的风险，包括但不限于账号被ban，下载限速等，与本程序无关；\n- 如有侵权，请通过[邮件](mailto:i@nn.ci)与我联系，会及时处理。\n\n---\n\n> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)\n"
  },
  {
    "path": "README_ja.md",
    "content": "<div align=\"center\">\n  <a href=\"https://alistgo.com\"><img width=\"100px\" alt=\"logo\" src=\"https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg\"/></a>\n  <p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>\n<div>\n  <a href=\"https://goreportcard.com/report/github.com/alist-org/alist/v3\">\n    <img src=\"https://goreportcard.com/badge/github.com/alist-org/alist/v3\" alt=\"latest version\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/Xhofe/alist\" alt=\"License\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/actions?query=workflow%3ABuild\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main\" alt=\"Build status\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/release/Xhofe/alist\" alt=\"latest version\" />\n  </a>\n  <a title=\"Crowdin\" target=\"_blank\" href=\"https://crwd.in/alist\">\n    <img src=\"https://badges.crowdin.net/alist/localized.svg\">\n  </a>\n</div>\n<div>\n  <a href=\"https://github.com/alist-org/alist/discussions\">\n    <img src=\"https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://discord.gg/F4ymsH4xv2\">\n    <img src=\"https://img.shields.io/discord/1018870125102895134?logo=discord\" alt=\"discussions\" />\n  </a>\n  <a href=\"https://github.com/alist-org/alist/releases\">\n    <img src=\"https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://hub.docker.com/r/xhofe/alist\">\n    <img src=\"https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://alistgo.com/guide/sponsor.html\">\n    <img src=\"https://img.shields.io/badge/%24-sponsor-F87171.svg\" alt=\"sponsor\" />\n  </a>\n</div>\n</div>\n\n---\n\n[English](./README.md) | [中文](./README_cn.md) | 日本語 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)\n\n## 特徴\n\n- [x] マルチストレージ\n    - [x] ローカルストレージ\n    - [x] [Aliyundrive](https://www.alipan.com/)\n    - [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [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\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(Support OneDrive/SharePoint without API)\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] [Aliyundrive share](https://www.alipan.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\n    - [x] [115](https://115.com/)\n    - [X] Cloudreve\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] デプロイが簡単で、すぐに使える\n- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)\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 (詳細は https://alistgo.com/guide/webdav.html を参照)\n- [x] [Docker デプロイ](https://hub.docker.com/r/xhofe/alist)\n- [x] Cloudflare ワーカープロキシ\n- [x] ファイル/フォルダパッケージのダウンロード\n- [x] ウェブアップロード(訪問者にアップロードを許可できる), 削除, mkdir, 名前変更, 移動, コピー\n- [x] オフラインダウンロード\n- [x] 二つのストレージ間でファイルをコピー\n- [x] シングルスレッドのダウンロード/ストリーム向けのマルチスレッド ダウンロード アクセラレーション\n\n## ドキュメント\n\n<https://alistgo.com/>\n\n## APIドキュメント（Apifox 提供）\n\n<https://alist-public.apifox.cn/>\n\n## デモ\n\n<https://al.nn.ci>\n\n## ディスカッション\n\n一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**\n\n## スポンサー\n\nAList はオープンソースのソフトウェアです。もしあなたがこのプロジェクトを気に入ってくださり、続けて欲しいと思ってくださるなら、ぜひスポンサーになってくださるか、1口でも寄付をしてくださるようご検討ください！すべての愛とサポートに感謝します:\nhttps://alistgo.com/guide/sponsor.html\n\n### スペシャルスポンサー\n\n- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.\n\n## コントリビューター\n\nこれらの素晴らしい人々に感謝します:\n\n[![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)\n\n## ライセンス\n\n`AList` は AGPL-3.0 ライセンスの下でライセンスされたオープンソースソフトウェアです。\n\n## 免責事項\n- このプログラムはフリーでオープンソースのプロジェクトです。ネットワークディスク上でファイルを共有するように設計されており、golang のダウンロードや学習に便利です。利用にあたっては関連法規を遵守し、悪用しないようお願いします;\n- このプログラムは、公式インターフェースの動作を破壊することなく、公式 sdk/インターフェースを呼び出すことで実装されています;\n- このプログラムは、302リダイレクト/トラフィック転送のみを行い、いかなるユーザーデータも傍受、保存、改ざんしません;\n- このプログラムを使用する前に、アカウントの禁止、ダウンロード速度の制限など、対応するリスクを理解し、負担する必要があります;\n- もし侵害があれば、[メール](mailto:i@nn.ci)で私に連絡してください。\n\n---\n\n> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)\n"
  },
  {
    "path": "build.sh",
    "content": "appName=\"alist\"\nbuiltAt=\"$(date +'%F %T %z')\"\ngitAuthor=\"Xhofe <i@nn.ci>\"\ngitCommit=$(git log --pretty=format:\"%h\" -1)\n\nif [ \"$1\" = \"dev\" ]; then\n  version=\"dev\"\n  webVersion=\"dev\"\nelif [ \"$1\" = \"beta\" ]; then\n  version=\"beta\"\n  webVersion=\"dev\"\nelse\n  git tag -d beta\n  version=$(git describe --abbrev=0 --tags)\n  webVersion=$(wget -qO- -t1 -T2 \"https://api.github.com/repos/alist-org/alist-web/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\"\n\nldflags=\"\\\n-w -s \\\n-X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \\\n-X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \\\n-X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \\\n-X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \\\n-X 'github.com/alist-org/alist/v3/internal/conf.WebVersion=$webVersion' \\\n\"\n\nFetchWebDev() {\n  curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -o web-dist-dev.tar.gz\n  tar -zxvf web-dist-dev.tar.gz\n  rm -rf public/dist\n  mv -f web-dist-dev/dist public\n  rm -rf web-dist-dev web-dist-dev.tar.gz\n}\n\nFetchWebRelease() {\n  curl -L https://github.com/alist-org/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz\n  tar -zxvf dist.tar.gz\n  rm -rf public/dist\n  mv -f dist public\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\nBuildDev() {\n  rm -rf .git/\n  mkdir -p \"dist\"\n  muslflags=\"--extldflags '-static -fpic' $ldflags\"\n  BASE=\"https://musl.nn.ci/\"\n  FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    curl -L -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 alist-* dist\n  cd dist\n  cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe\n  upx -9 ./alist-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/alist -ldflags=\"$ldflags\" -tags=jsoniter .\n}\n\nPrepareBuildDockerMusl() {\n  mkdir -p build/musl-libs\n  BASE=\"https://github.com/go-cross/musl-toolchain-archive/releases/latest/download/\"\n  FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    lib_tgz=\"build/${i}.tgz\"\n    curl -L -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-s390x linux-riscv64 linux-ppc64le)\n  CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc)\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/alist -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##*-}/alist -ldflags=\"$docker_lflags\" -tags=jsoniter .\n  done\n}\n\nBuildRelease() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  BuildWinArm64 ./build/alist-windows-arm64.exe\n  xgo -out \"$appName\" -ldflags=\"$ldflags\" -tags=jsoniter .\n  # why? Because some target platforms seem to have issues with upx compression\n  upx -9 ./alist-linux-amd64\n  cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe\n  upx -9 ./alist-windows-amd64-upx.exe\n  mv alist-* build\n}\n\nBuildReleaseLinuxMusl() {\n  rm -rf .git/\n  mkdir -p \"build\"\n  muslflags=\"--extldflags '-static -fpic' $ldflags\"\n  BASE=\"https://musl.nn.ci/\"\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)\n  for i in \"${FILES[@]}\"; do\n    url=\"${BASE}${i}.tgz\"\n    curl -L -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)\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)\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://musl.nn.ci/\"\n#  FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armeb-linux-musleabi-cross armeb-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  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 -L -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-armeb linux-musleabihf-armeb 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 armeb-linux-musleabi-gcc armeb-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  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\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  OS_ARCHES=(amd64 arm64 i386)\n  GO_ARCHES=(amd64 arm64 386)\n  CGO_ARGS=(x86_64-unknown-freebsd14.1 aarch64-unknown-freebsd14.1 i386-unknown-freebsd14.1)\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}/14.3-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  mkdir compress\n  for i in $(find . -type f -name \"$appName-linux-*\"); do\n    cp \"$i\" alist\n    tar -czvf compress/\"$i\".tar.gz alist\n    rm -f alist\n  done\n    for i in $(find . -type f -name \"$appName-android-*\"); do\n    cp \"$i\" alist\n    tar -czvf compress/\"$i\".tar.gz alist\n    rm -f alist\n  done\n  for i in $(find . -type f -name \"$appName-darwin-*\"); do\n    cp \"$i\" alist\n    tar -czvf compress/\"$i\".tar.gz alist\n    rm -f alist\n  done\n  for i in $(find . -type f -name \"$appName-freebsd-*\"); do\n    cp \"$i\" alist\n    tar -czvf compress/\"$i\".tar.gz alist\n    rm -f alist\n  done\n  for i in $(find . -type f -name \"$appName-windows-*\"); do\n    cp \"$i\" alist.exe\n    zip compress/$(echo $i | sed 's/\\.[^.]*$//').zip alist.exe\n    rm -f alist.exe\n  done\n  cd compress\n  find . -type f -print0 | xargs -0 md5sum >\"$1\"\n  cat \"$1\"\n  cd ../..\n}\n\nif [ \"$1\" = \"dev\" ]; then\n  FetchWebDev\n  if [ \"$2\" = \"docker\" ]; then\n    BuildDocker\n  elif [ \"$2\" = \"docker-multiplatform\" ]; then\n      BuildDockerMultiplatform\n  elif [ \"$2\" = \"web\" ]; then\n    echo \"web only\"\n  else\n    BuildDev\n  fi\nelif [ \"$1\" = \"release\" -o \"$1\" = \"beta\" ]; then\n  if [ \"$1\" = \"beta\" ]; then\n    FetchWebDev\n  else\n    FetchWebRelease\n  fi\n  if [ \"$2\" = \"docker\" ]; then\n    BuildDocker\n  elif [ \"$2\" = \"docker-multiplatform\" ]; then\n    BuildDockerMultiplatform\n  elif [ \"$2\" = \"linux_musl_arm\" ]; then\n    BuildReleaseLinuxMuslArm\n    MakeRelease \"md5-linux-musl-arm.txt\"\n  elif [ \"$2\" = \"linux_musl\" ]; then\n    BuildReleaseLinuxMusl\n    MakeRelease \"md5-linux-musl.txt\"\n  elif [ \"$2\" = \"android\" ]; then\n    BuildReleaseAndroid\n    MakeRelease \"md5-android.txt\"\n  elif [ \"$2\" = \"freebsd\" ]; then\n    BuildReleaseFreeBSD\n    MakeRelease \"md5-freebsd.txt\"\n  elif [ \"$2\" = \"web\" ]; then\n    echo \"web only\"\n  else\n    BuildRelease\n    MakeRelease \"md5.txt\"\n  fi\nelif [ \"$1\" = \"prepare\" ]; then\n  if [ \"$2\" = \"docker-multiplatform\" ]; then\n    PrepareBuildDockerMusl\n  fi\nelif [ \"$1\" = \"zip\" ]; then\n  MakeRelease \"$2\".txt\nelse\n  echo -e \"Parameter error\"\nfi\n"
  },
  {
    "path": "cmd/admin.go",
    "content": "/*\nCopyright © 2022 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\tInit()\n\t\tdefer 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(\"Admin user's username: %s\", admin.Username)\n\t\t\tutils.Log.Infof(\"The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed\")\n\t\t\tutils.Log.Infof(\"You can reset the password with a random string by running [alist admin random]\")\n\t\t\tutils.Log.Infof(\"You can also set a new password by running [alist 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\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\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) == 0 {\n\t\t\tutils.Log.Errorf(\"Please enter the new password\")\n\t\t\treturn\n\t\t}\n\t\tsetAdminPassword(args[0])\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\tInit()\n\t\tdefer Release()\n\t\ttoken := setting.GetStr(conf.Token)\n\t\tutils.Log.Infof(\"Admin token: %s\", token)\n\t},\n}\n\nfunc setAdminPassword(pwd string) {\n\tInit()\n\tdefer 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 updated:\")\n\tutils.Log.Infof(\"username: %s\", admin.Username)\n\tutils.Log.Infof(\"password: %s\", 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\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\tInit()\n\t\tdefer 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.Info(\"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\"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/internal/bootstrap\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/data\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc Init() {\n\tbootstrap.InitConfig()\n\tbootstrap.Log()\n\tbootstrap.InitDB()\n\n\tif v3_46_0.IsLegacyRoleDetected() {\n\t\tutils.Log.Warnf(\"Detected legacy role format, executing ConvertLegacyRoles patch early...\")\n\t\tv3_46_0.ConvertLegacyRoles()\n\t}\n\n\tdata.InitData()\n\tbootstrap.InitStreamLimit()\n\tbootstrap.InitIndex()\n\tbootstrap.InitUpgradePatch()\n}\n\nfunc Release() {\n\tdb.Close()\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/flags/config.go",
    "content": "package flags\n\nvar (\n\tDataDir     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\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"os\"\n)\n\n// KillCmd represents the kill command\nvar KillCmd = &cobra.Command{\n\tUse:   \"kill\",\n\tShort: \"Force kill alist 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 `alist 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\"reflect\"\n\t\"strings\"\n\n\t_ \"github.com/alist-org/alist/v3/drivers\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/data\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\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(\"../alist-web/src/lang/en/%s.json\", 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 reflect.DeepEqual(oldData, newData) {\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), newData, true)\n\t}\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\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// 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 alist 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/alist-org/alist/v3/cmd/flags\"\n\t_ \"github.com/alist-org/alist/v3/drivers\"\n\t_ \"github.com/alist-org/alist/v3/internal/archive\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar RootCmd = &cobra.Command{\n\tUse:   \"alist\",\n\tShort: \"A file list program that supports multiple storage.\",\n\tLong: `A file list program that supports multiple storage,\nbuilt with love by Xhofe and friends in Go/Solid.js.\nComplete documentation is available at https://alistgo.com/`,\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 folder\")\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\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"github.com/KirCute/sftpd-alist\"\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\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\tInit()\n\t\tif conf.Conf.DelayedStart != 0 {\n\t\t\tutils.Log.Infof(\"delayed start for %d seconds\", conf.Conf.DelayedStart)\n\t\t\ttime.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)\n\t\t}\n\t\tbootstrap.InitOfflineDownloadTools()\n\t\tbootstrap.LoadStorages()\n\t\tbootstrap.InitTaskManager()\n\t\tif !flags.Debug && !flags.Dev {\n\t\t\tgin.SetMode(gin.ReleaseMode)\n\t\t}\n\t\tr := gin.New()\n\t\tr.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))\n\t\tserver.Init(r)\n\t\tvar httpHandler http.Handler = r\n\t\tif conf.Conf.Scheme.EnableH2c {\n\t\t\thttpHandler = h2c.NewHandler(r, &http2.Server{})\n\t\t}\n\t\tvar httpSrv, httpsSrv, unixSrv *http.Server\n\t\tif conf.Conf.Scheme.HttpPort != -1 {\n\t\t\thttpBase := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)\n\t\t\tutils.Log.Infof(\"start HTTP server @ %s\", httpBase)\n\t\t\thttpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}\n\t\t\tgo func() {\n\t\t\t\terr := httpSrv.ListenAndServe()\n\t\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\t\tutils.Log.Fatalf(\"failed to start http: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.Scheme.HttpsPort != -1 {\n\t\t\thttpsBase := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)\n\t\t\tutils.Log.Infof(\"start HTTPS server @ %s\", httpsBase)\n\t\t\thttpsSrv = &http.Server{Addr: httpsBase, Handler: r}\n\t\t\tgo func() {\n\t\t\t\terr := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)\n\t\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\t\tutils.Log.Fatalf(\"failed to start https: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.Scheme.UnixFile != \"\" {\n\t\t\tutils.Log.Infof(\"start unix server @ %s\", conf.Conf.Scheme.UnixFile)\n\t\t\tunixSrv = &http.Server{Handler: httpHandler}\n\t\t\tgo func() {\n\t\t\t\tlistener, err := net.Listen(\"unix\", conf.Conf.Scheme.UnixFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.Log.Fatalf(\"failed to listen unix: %+v\", err)\n\t\t\t\t}\n\t\t\t\t// set socket file permission\n\t\t\t\tmode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.Log.Errorf(\"failed to parse socket file permission: %+v\", err)\n\t\t\t\t} else {\n\t\t\t\t\terr = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.Log.Errorf(\"failed to chmod socket file: %+v\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terr = unixSrv.Serve(listener)\n\t\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\t\tutils.Log.Fatalf(\"failed to start unix: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {\n\t\t\ts3r := gin.New()\n\t\t\ts3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))\n\t\t\tserver.InitS3(s3r)\n\t\t\ts3Base := fmt.Sprintf(\"%s:%d\", conf.Conf.Scheme.Address, conf.Conf.S3.Port)\n\t\t\tutils.Log.Infof(\"start S3 server @ %s\", s3Base)\n\t\t\tgo func() {\n\t\t\t\tvar err error\n\t\t\t\tif conf.Conf.S3.SSL {\n\t\t\t\t\thttpsSrv = &http.Server{Addr: s3Base, Handler: s3r}\n\t\t\t\t\terr = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)\n\t\t\t\t}\n\t\t\t\tif !conf.Conf.S3.SSL {\n\t\t\t\t\thttpSrv = &http.Server{Addr: s3Base, Handler: s3r}\n\t\t\t\t\terr = httpSrv.ListenAndServe()\n\t\t\t\t}\n\t\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\t\tutils.Log.Fatalf(\"failed to start s3 server: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tvar ftpDriver *server.FtpMainDriver\n\t\tvar ftpServer *ftpserver.FtpServer\n\t\tif conf.Conf.FTP.Listen != \"\" && conf.Conf.FTP.Enable {\n\t\t\tvar err error\n\t\t\tftpDriver, err = server.NewMainDriver()\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Fatalf(\"failed to start ftp driver: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\tutils.Log.Infof(\"start ftp server on %s\", conf.Conf.FTP.Listen)\n\t\t\t\tgo func() {\n\t\t\t\t\tftpServer = ftpserver.NewFtpServer(ftpDriver)\n\t\t\t\t\terr = ftpServer.ListenAndServe()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.Log.Fatalf(\"problem ftp server listening: %s\", err.Error())\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t}\n\t\tvar sftpDriver *server.SftpDriver\n\t\tvar sftpServer *sftpd.SftpServer\n\t\tif conf.Conf.SFTP.Listen != \"\" && conf.Conf.SFTP.Enable {\n\t\t\tvar err error\n\t\t\tsftpDriver, err = server.NewSftpDriver()\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Fatalf(\"failed to start sftp driver: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\tutils.Log.Infof(\"start sftp server on %s\", conf.Conf.SFTP.Listen)\n\t\t\t\tgo func() {\n\t\t\t\t\tsftpServer = sftpd.NewSftpServer(sftpDriver)\n\t\t\t\t\terr = sftpServer.RunServer()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.Log.Fatalf(\"problem sftp server listening: %s\", err.Error())\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t}\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\tutils.Log.Println(\"Shutdown server...\")\n\t\tfs.ArchiveContentUploadTaskManager.RemoveAll()\n\t\tRelease()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)\n\t\tdefer cancel()\n\t\tvar wg sync.WaitGroup\n\t\tif conf.Conf.Scheme.HttpPort != -1 {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif err := httpSrv.Shutdown(ctx); err != nil {\n\t\t\t\t\tutils.Log.Fatal(\"HTTP server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.Scheme.HttpsPort != -1 {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif err := httpsSrv.Shutdown(ctx); err != nil {\n\t\t\t\t\tutils.Log.Fatal(\"HTTPS server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.Scheme.UnixFile != \"\" {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif err := unixSrv.Shutdown(ctx); err != nil {\n\t\t\t\t\tutils.Log.Fatal(\"Unix server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.FTP.Listen != \"\" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tftpDriver.Stop()\n\t\t\t\tif err := ftpServer.Stop(); err != nil {\n\t\t\t\t\tutils.Log.Fatal(\"FTP server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\tif conf.Conf.SFTP.Listen != \"\" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif err := sftpServer.Close(); err != nil {\n\t\t\t\t\tutils.Log.Fatal(\"SFTP server shutdown err: \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t\tutils.Log.Println(\"Server exit\")\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// OutAlistInit 暴露用于外部启动server的函数\nfunc OutAlistInit() {\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 alist 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(\"alist 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 `./alist 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 alist 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 `alist 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\"os\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/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\",\n\tShort: \"Disable a storage\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) < 1 {\n\t\t\tutils.Log.Errorf(\"mount path is required\")\n\t\t\treturn\n\t\t}\n\t\tmountPath := args[0]\n\t\tInit()\n\t\tdefer Release()\n\t\tstorage, err := db.GetStorageByMountPath(mountPath)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to query storage: %+v\", err)\n\t\t} else {\n\t\t\tstorage.Disabled = true\n\t\t\terr = db.UpdateStorage(storage)\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"failed to update storage: %+v\", err)\n\t\t\t} else {\n\t\t\t\tutils.Log.Infof(\"Storage with mount path [%s] have been disabled\", mountPath)\n\t\t\t}\n\t\t}\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\tRun: func(cmd *cobra.Command, args []string) {\n\t\tInit()\n\t\tdefer Release()\n\t\tstorages, _, err := db.GetStorages(1, -1)\n\t\tif err != nil {\n\t\t\tutils.Log.Errorf(\"failed to query storages: %+v\", err)\n\t\t} else {\n\t\t\tutils.Log.Infof(\"Found %d storages\", 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\tutils.Log.Errorf(\"failed to run program: %+v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\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\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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 AList\",\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": "version: '3.3'\nservices:\n  alist:\n    restart: always\n    volumes:\n      - '/etc/alist:/opt/alist/data'\n    ports:\n      - '5244:5244'\n      - '5245:5245'\n    environment:\n      - PUID=0\n      - PGID=0\n      - UMASK=022\n      - TZ=UTC\n    container_name: alist\n    image: 'xhofe/alist:latest'\n"
  },
  {
    "path": "drivers/115/appver.go",
    "content": "package _115\n\nimport (\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tmd5Salt = \"Qclm8MGWUv59TnrR0XPg\"\n\tappVer  = \"27.0.5.7\"\n)\n\nfunc (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {\n\tresult := driver115.VersionResp{}\n\tresp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)\n\n\terr = driver115.CheckErr(err, &result, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data.GetAppVersions(), nil\n}\n\nfunc (d *Pan115) getAppVer() string {\n\t// todo add some cache？\n\tvers, 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\tfor _, ver := range vers {\n\t\tif ver.AppName == \"win\" {\n\t\t\treturn ver.Version\n\t\t}\n\t}\n\treturn appVer\n}\n\nfunc (d *Pan115) initAppVer() {\n\tappVer = d.getAppVer()\n}\n"
  },
  {
    "path": "drivers/115/driver.go",
    "content": "package _115\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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 := \"\"\n\tif args.Header != nil {\n\t\tuserAgent = args.Header.Get(\"User-Agent\")\n\t}\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) <= 0 {\n\t\ttmpF, err := stream.CacheFullInTempFile()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfullHash, err = utils.HashFile(utils.SHA1, tmpF)\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\nvar _ driver.Driver = (*Pan115)(nil)\n"
  },
  {
    "path": "drivers/115/meta.go",
    "content": "package _115\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\t// OnlyProxy:   true,\n\t// OnlyLocal:         true,\n\t// NoOverwriteUpload: true,\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/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\nvar _ model.Obj = (*FileObj)(nil)\n\ntype FileObj struct {\n\tdriver.File\n\tThumbURL string\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\n\tcipher \"github.com/SheltonZhu/115driver/pkg/crypto/ec115\"\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/pkg/errors\"\n)\n\ntype fileInfoWithThumb struct {\n\tdriver115.FileInfo\n\tThumbURL string `json:\"u\"`\n}\n\ntype fileListRespWithThumb struct {\n\tdriver115.BasicResp\n\tCategoryID driver115.IntString `json:\"cid\"`\n\tCount      int                 `json:\"count\"`\n\tOffset     int                 `json:\"offset\"`\n\tFiles      []fileInfoWithThumb `json:\"data\"`\n}\n\ntype getFileInfoResponseWithThumb struct {\n\tdriver115.BasicResp\n\tFiles []*fileInfoWithThumb `json:\"data\"`\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\tlimit := d.PageSize\n\tif limit > driver115.MaxDirPageLimit {\n\t\tlimit = driver115.MaxDirPageLimit\n\t}\n\n\topts := driver115.DefaultListOptions()\n\tdriver115.WithMultiUrls()(opts)\n\tif len(opts.ApiURLs) == 0 {\n\t\topts.ApiURLs = []string{driver115.ApiFileList}\n\t}\n\n\toffset := int64(0)\n\tfor i := 0; ; i++ {\n\t\tresult, err := d.getFilesPageWithThumb(fileId, opts.ApiURLs[i%len(opts.ApiURLs)], limit, offset)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, fileInfo := range result.Files {\n\t\t\tres = append(res, fileObjFromInfo(&fileInfo))\n\t\t}\n\t\toffset = int64(result.Offset) + limit\n\t\tif offset >= int64(result.Count) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc (d *Pan115) getNewFile(fileId string) (*FileObj, error) {\n\tfileInfo, err := d.getFileInfoWithThumb(\"file_id\", fileId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfile := fileObjFromInfo(fileInfo)\n\treturn &file, nil\n}\n\nfunc (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {\n\tfileInfo, err := d.getFileInfoWithThumb(\"pick_code\", pickCode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfile := fileObjFromInfo(fileInfo)\n\treturn &file, nil\n}\n\nfunc (d *Pan115) getUA() string {\n\treturn fmt.Sprintf(\"Mozilla/5.0 115Browser/%s\", appVer)\n}\n\nfunc fileObjFromInfo(fileInfo *fileInfoWithThumb) FileObj {\n\tfile := &driver115.File{}\n\tfile.From(&fileInfo.FileInfo)\n\treturn FileObj{\n\t\tFile:     *file,\n\t\tThumbURL: fileInfo.ThumbURL,\n\t}\n}\n\nfunc (d *Pan115) getFileInfoWithThumb(queryKey, queryVal string) (*fileInfoWithThumb, error) {\n\tresult := getFileInfoResponseWithThumb{}\n\treq := d.client.NewRequest().\n\t\tSetQueryParam(queryKey, queryVal).\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\treturn result.Files[0], nil\n}\n\nfunc (d *Pan115) getFilesPageWithThumb(dirID, apiURL string, limit, offset int64) (*fileListRespWithThumb, error) {\n\tif dirID == \"\" {\n\t\tdirID = \"0\"\n\t}\n\tresult := fileListRespWithThumb{}\n\tparams := map[string]string{\n\t\t\"aid\":              \"1\",\n\t\t\"cid\":              dirID,\n\t\t\"o\":                driver115.FileOrderByTime,\n\t\t\"asc\":              \"1\",\n\t\t\"offset\":           strconv.FormatInt(offset, 10),\n\t\t\"show_dir\":         \"1\",\n\t\t\"limit\":            strconv.FormatInt(limit, 10),\n\t\t\"snap\":             \"0\",\n\t\t\"natsort\":          \"0\",\n\t\t\"record_open_time\": \"1\",\n\t\t\"format\":           \"json\",\n\t\t\"fc_mix\":           \"0\",\n\t}\n\treq := d.client.NewRequest().\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetQueryParams(params).\n\t\tSetResult(&result)\n\tresp, err := req.Get(apiURL)\n\tif err := driver115.CheckErr(err, &result, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif dirID != string(result.CategoryID) {\n\t\treturn nil, driver115.ErrUnexpected\n\t}\n\treturn &result, nil\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 := oss.New(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) (*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.CacheFullInTempFile()\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 = oss.New(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\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tsdk \"github.com/xhofe/115-sdk-go\"\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\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(200)\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\tFileNma: 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\tif err := d.WaitLimit(ctx); err != nil {\n\t\treturn err\n\t}\n\ttempF, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// cal full sha1\n\tsha1, err := utils.HashReader(utils.SHA1, tempF)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = tempF.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// pre 128k sha1\n\tsha1128k, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, 128*1024))\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = tempF.Seek(0, io.SeekStart)\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\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\t_, err = tempF.Seek(start, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsignVal, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, end-start+1))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = tempF.Seek(0, io.SeekStart)\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\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, tempF, file, up, tokenResp, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\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\tAccessToken    string\n}\n\nvar config = driver.Config{\n\tName:              \"115 Open\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tsdk \"github.com/xhofe/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\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/aliyun/aliyun-oss-go-sdk/oss\"\n\t\"github.com/avast/retry-go\"\n\tsdk \"github.com/xhofe/115-sdk-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 := oss.New(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, tempF model.File, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {\n\tfileSize := stream.GetSize()\n\tchunkSize := calPartSize(fileSize)\n\n\tossClient, err := oss.New(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\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 := utils.NewMultiReadable(io.LimitReader(stream, partSize))\n\t\terr = retry.Do(func() error {\n\t\t\t_ = rd.Reset()\n\t\t\trateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)\n\t\t\tpart, err := bucket.UploadPart(imur, rateLimitedRd, 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.Attempts(3),\n\t\t\tretry.DelayType(retry.BackOffDelay),\n\t\t\tretry.Delay(time.Second))\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) / 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\n// do others that not defined in Driver interface\n"
  },
  {
    "path": "drivers/115_share/driver.go",
    "content": "package _115_share\n\nimport (\n\t\"context\"\n\n\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\n\tua := base.UserAgent\n\tfiles := make([]shareFile, 0)\n\tfileResp, err := d.getShareSnapWithUA(ua, 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.getShareSnapWithUA(ua, dir.GetID(), driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count))\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\tua := \"\"\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.downloadByShareCodeWithUA(ua, 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\t// OnlyProxy:   true,\n\t// OnlyLocal:         true,\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: true,\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\tdriver115 \"github.com/SheltonZhu/115driver/pkg/driver\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\ntype shareFile struct {\n\tFileID     string                 `json:\"fid\"`\n\tUID        int                    `json:\"uid\"`\n\tCategoryID driver115.IntString    `json:\"cid\"`\n\tFileName   string                 `json:\"n\"`\n\tType       string                 `json:\"ico\"`\n\tSha1       string                 `json:\"sha\"`\n\tSize       driver115.StringInt64  `json:\"s\"`\n\tLabels     []*driver115.LabelInfo `json:\"fl\"`\n\tUpdateTime string                 `json:\"t\"`\n\tIsFile     int                    `json:\"fc\"`\n\tParentID   string                 `json:\"pid\"`\n\tThumbURL   string                 `json:\"u\"`\n}\n\ntype shareSnapResp struct {\n\tdriver115.BasicResp\n\tData struct {\n\t\tCount int         `json:\"count\"`\n\t\tList  []shareFile `json:\"list\"`\n\t} `json:\"data\"`\n}\n\ntype downloadShareResp struct {\n\tdriver115.BasicResp\n\tData driver115.SharedDownloadInfo `json:\"data\"`\n}\n\nfunc transFunc(sf 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 buildShareReferer(shareCode, receiveCode string) string {\n\treturn fmt.Sprintf(\"https://115cdn.com/s/%s?password=%s&\", shareCode, receiveCode)\n}\n\nfunc (d *Pan115Share) getShareSnapWithUA(ua, dirID string, queries ...driver115.Query) (*shareSnapResp, error) {\n\tresult := shareSnapResp{}\n\tquery := map[string]string{\n\t\t\"share_code\":   d.ShareCode,\n\t\t\"receive_code\": d.ReceiveCode,\n\t\t\"cid\":          dirID,\n\t\t\"limit\":        \"20\",\n\t\t\"asc\":          \"0\",\n\t\t\"offset\":       \"0\",\n\t\t\"format\":       \"json\",\n\t}\n\tfor _, q := range queries {\n\t\tq(&query)\n\t}\n\n\treq := d.client.NewRequest().\n\t\tSetQueryParams(query).\n\t\tSetHeader(\"referer\", buildShareReferer(d.ShareCode, d.ReceiveCode)).\n\t\tForceContentType(\"application/json;charset=UTF-8\").\n\t\tSetResult(&result)\n\tif ua != \"\" {\n\t\treq = req.SetHeader(\"User-Agent\", ua)\n\t}\n\n\tresp, err := req.Get(driver115.ApiShareSnap)\n\tif err := driver115.CheckErr(err, &result, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &result, nil\n}\n\nfunc (d *Pan115Share) downloadByShareCodeWithUA(ua, fileID string) (*driver115.SharedDownloadInfo, error) {\n\tresult := downloadShareResp{}\n\tparams := map[string]string{\n\t\t\"share_code\":   d.ShareCode,\n\t\t\"receive_code\": d.ReceiveCode,\n\t\t\"file_id\":      fileID,\n\t\t\"dl\":           \"1\",\n\t}\n\n\treq := d.client.NewRequest().\n\t\tSetQueryParams(params).\n\t\tForceContentType(\"application/json\").\n\t\tSetHeader(\"referer\", buildShareReferer(d.ShareCode, d.ReceiveCode)).\n\t\tSetResult(&result)\n\tif ua != \"\" {\n\t\treq = req.SetHeader(\"User-Agent\", ua)\n\t}\n\n\tresp, err := req.Get(driver115.ApiDownloadGetShareUrl)\n\tif err := driver115.CheckErr(err, &result, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &result.Data, nil\n}\n\nfunc (d *Pan115Share) login() error {\n\tvar err error\n\topts := []driver115.Option{\n\t\tdriver115.UA(base.UserAgent),\n\t}\n\td.client = driver115.New(opts...)\n\tif _, err = d.getShareSnapWithUA(base.UserAgent, \"\"); 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\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/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\tsafeBoxUnlocked 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, nil, 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\tif f, ok := dir.(File); ok && f.IsLock {\n\t\tif err := d.unlockSafeBox(f.FileId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfiles, err := d.getFiles(ctx, dir.GetID(), dir.GetName())\n\tif err != nil {\n\t\tmsg := strings.ToLower(err.Error())\n\t\tif strings.Contains(msg, \"safe box\") || strings.Contains(err.Error(), \"保险箱\") {\n\t\t\tif id, e := strconv.ParseInt(dir.GetID(), 10, 64); e == nil {\n\t\t\t\tif e = d.unlockSafeBox(id); e == nil {\n\t\t\t\t\tfiles, err = d.getFiles(ctx, dir.GetID(), dir.GetName())\n\t\t\t\t} else {\n\t\t\t\t\treturn nil, e\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\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\t//var resp DownResp\n\t\tvar headers map[string]string\n\t\tif !utils.IsLocalIPAddr(args.IP) {\n\t\t\theaders = map[string]string{\n\t\t\t\t//\"X-Real-IP\":       \"1.1.1.1\",\n\t\t\t\t\"X-Forwarded-For\": args.IP,\n\t\t\t}\n\t\t}\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\n\t\t\treq.SetBody(data).SetHeaders(headers)\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\tu, err := url.Parse(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnu := u.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}\n\t\tu_ := u.String()\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{\"https://www.123pan.com/\"},\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.CacheFullInTempFileAndHash(file, 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\":         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\nvar _ driver.Driver = (*Pan123)(nil)\n"
  },
  {
    "path": "drivers/123/meta.go",
    "content": "package _123\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tUsername     string `json:\"username\" required:\"true\"`\n\tPassword     string `json:\"password\" required:\"true\"`\n\tSafePassword string `json:\"safe_password\"`\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}\n\nvar config = driver.Config{\n\tName:        \"123Pan\",\n\tDefaultRoot: \"0\",\n\tLocalSort:   true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Pan123{}\n\t})\n}\n"
  },
  {
    "path": "drivers/123/types.go",
    "content": "package _123\n\nimport (\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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\tIsLock      bool      `json:\"IsLock\"`\n}\n\nfunc (f File) CreateTime() time.Time {\n\treturn f.UpdateAt\n}\n\nfunc (f File) GetHash() utils.HashInfo {\n\treturn utils.HashInfo{}\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"
  },
  {
    "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\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\ttmpF, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// fetch s3 pre signed urls\n\tsize := file.GetSize()\n\tchunkSize := min(size, 16*utils.MB)\n\tchunkCount := int(size / chunkSize)\n\tlastChunkSize := size % chunkSize\n\tif lastChunkSize > 0 {\n\t\tchunkCount++\n\t} else {\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\tfor i := 1; i <= chunkCount; i += batchSize {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tstart := i\n\t\tend := min(i+batchSize, chunkCount+1)\n\t\ts3PreSignedUrls, err := getS3UploadUrl(ctx, 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 j := start; j < end; j++ {\n\t\t\tif utils.IsCanceled(ctx) {\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\t\t\tcurSize := chunkSize\n\t\t\tif j == chunkCount {\n\t\t\t\tcurSize = lastChunkSize\n\t\t\t}\n\t\t\terr = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.NewSectionReader(tmpF, chunkSize*int64(j-1), curSize), curSize, false, getS3UploadUrl)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tup(float64(j) * 100 / float64(chunkCount))\n\t\t}\n\t}\n\t// complete s3 upload\n\treturn d.completeS3(ctx, upReq, file, chunkCount > 1)\n}\n\nfunc (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader *io.SectionReader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error {\n\tuploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]\n\tif uploadUrl == \"\" {\n\t\treturn fmt.Errorf(\"upload url is empty, s3PreSignedUrls: %+v\", s3PreSignedUrls)\n\t}\n\treq, err := http.NewRequest(\"PUT\", uploadUrl, driver.NewLimitedUploadStream(ctx, reader))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq = req.WithContext(ctx)\n\treq.ContentLength = curSize\n\t//req.Header.Set(\"Content-Length\", strconv.FormatInt(curSize, 10))\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.StatusForbidden {\n\t\tif retry {\n\t\t\treturn fmt.Errorf(\"upload s3 chunk %d failed, status code: %d\", cur, res.StatusCode)\n\t\t}\n\t\t// refresh s3 pre signed urls\n\t\tnewS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls\n\t\t// retry\n\t\treader.Seek(0, io.SeekStart)\n\t\treturn d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl)\n\t}\n\tif res.StatusCode != http.StatusOK {\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn fmt.Errorf(\"upload s3 chunk %d failed, status code: %d, body: %s\", cur, res.StatusCode, body)\n\t}\n\treturn nil\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\tSafeBoxUnlock    = MainApi + \"/restful/goapi/v1/file/safe_box/auth/unlockbox\"\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\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)-alist\",\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)\",\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\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) unlockSafeBox(fileId int64) error {\n\tif _, ok := d.safeBoxUnlocked.Load(fileId); ok {\n\t\treturn nil\n\t}\n\tdata := base.Json{\"password\": d.SafePassword}\n\turl := fmt.Sprintf(\"%s?fileId=%d\", SafeBoxUnlock, fileId)\n\t_, err := d.Request(url, 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\td.safeBoxUnlocked.Store(fileId, true)\n\treturn nil\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\tmsg := strings.ToLower(err.Error())\n\t\t\tif strings.Contains(msg, \"safe box\") || strings.Contains(err.Error(), \"保险箱\") {\n\t\t\t\tif fid, e := strconv.ParseInt(parentId, 10, 64); e == nil {\n\t\t\t\t\tif e = d.unlockSafeBox(fid); e == nil {\n\t\t\t\t\t\treturn d.getFiles(ctx, parentId, name)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, e\n\t\t\t\t}\n\t\t\t}\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"
  },
  {
    "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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 (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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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/api.go",
    "content": "package _123Open\n\nimport (\n\t\"fmt\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"net/http\"\n)\n\nconst (\n\t// baseurl\n\tApiBaseURL = \"https://open-api.123pan.com\"\n\n\t// auth\n\tApiToken = \"/api/v1/access_token\"\n\n\t// file list\n\tApiFileList = \"/api/v2/file/list\"\n\n\t// direct link\n\tApiGetDirectLink = \"/api/v1/direct-link/url\"\n\n\t// mkdir\n\tApiMakeDir = \"/upload/v1/file/mkdir\"\n\n\t// remove\n\tApiRemove = \"/api/v1/file/trash\"\n\n\t// upload\n\tApiUploadDomainURL   = \"/upload/v2/file/domain\"\n\tApiSingleUploadURL   = \"/upload/v2/file/single/create\"\n\tApiCreateUploadURL   = \"/upload/v2/file/create\"\n\tApiUploadSliceURL    = \"/upload/v2/file/slice\"\n\tApiUploadCompleteURL = \"/upload/v2/file/upload_complete\"\n\n\t// move\n\tApiMove = \"/api/v1/file/move\"\n\n\t// rename\n\tApiRename = \"/api/v1/file/name\"\n)\n\ntype Response[T any] struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    T      `json:\"data\"`\n}\n\ntype TokenResp struct {\n\tCode    int       `json:\"code\"`\n\tMessage string    `json:\"message\"`\n\tData    TokenData `json:\"data\"`\n}\n\ntype TokenData struct {\n\tAccessToken string `json:\"accessToken\"`\n\tExpiredAt   string `json:\"expiredAt\"`\n}\n\ntype FileListResp struct {\n\tCode    int          `json:\"code\"`\n\tMessage string       `json:\"message\"`\n\tData    FileListData `json:\"data\"`\n}\n\ntype FileListData struct {\n\tLastFileId int64  `json:\"lastFileId\"`\n\tFileList   []File `json:\"fileList\"`\n}\n\ntype DirectLinkResp struct {\n\tCode    int            `json:\"code\"`\n\tMessage string         `json:\"message\"`\n\tData    DirectLinkData `json:\"data\"`\n}\n\ntype DirectLinkData struct {\n\tURL string `json:\"url\"`\n}\n\ntype MakeDirRequest struct {\n\tName     string `json:\"name\"`\n\tParentID int64  `json:\"parentID\"`\n}\n\ntype MakeDirResp struct {\n\tCode    int         `json:\"code\"`\n\tMessage string      `json:\"message\"`\n\tData    MakeDirData `json:\"data\"`\n}\n\ntype MakeDirData struct {\n\tDirID int64 `json:\"dirID\"`\n}\n\ntype RemoveRequest struct {\n\tFileIDs []int64 `json:\"fileIDs\"`\n}\n\ntype UploadCreateResp struct {\n\tCode    int              `json:\"code\"`\n\tMessage string           `json:\"message\"`\n\tData    UploadCreateData `json:\"data\"`\n}\n\ntype UploadCreateData struct {\n\tFileID      int64    `json:\"fileId\"`\n\tReuse       bool     `json:\"reuse\"`\n\tPreuploadID string   `json:\"preuploadId\"`\n\tSliceSize   int64    `json:\"sliceSize\"`\n\tServers     []string `json:\"servers\"`\n}\n\ntype UploadUrlResp struct {\n\tCode    int           `json:\"code\"`\n\tMessage string        `json:\"message\"`\n\tData    UploadUrlData `json:\"data\"`\n}\n\ntype UploadUrlData struct {\n\tPresignedURL string `json:\"presignedUrl\"`\n}\n\ntype UploadCompleteResp struct {\n\tCode    int                `json:\"code\"`\n\tMessage string             `json:\"message\"`\n\tData    UploadCompleteData `json:\"data\"`\n}\n\ntype UploadCompleteData struct {\n\tFileID    int  `json:\"fileID\"`\n\tCompleted bool `json:\"completed\"`\n}\n\nfunc (d *Open123) Request(endpoint string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {\n\tclient := resty.New()\n\ttoken, err := d.tm.getToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := client.R().\n\t\tSetHeader(\"Authorization\", \"Bearer \"+token).\n\t\tSetHeader(\"Platform\", \"open_platform\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetResult(result)\n\n\tif setup != nil {\n\t\tsetup(req)\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\treturn req.Get(ApiBaseURL + endpoint)\n\tcase http.MethodPost:\n\t\treturn req.Post(ApiBaseURL + endpoint)\n\tcase http.MethodPut:\n\t\treturn req.Put(ApiBaseURL + endpoint)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported method: %s\", method)\n\t}\n}\n\nfunc (d *Open123) RequestTo(fullURL string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {\n\tclient := resty.New()\n\n\ttoken, err := d.tm.getToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := client.R().\n\t\tSetHeader(\"Authorization\", \"Bearer \"+token).\n\t\tSetHeader(\"Platform\", \"open_platform\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetResult(result)\n\n\tif setup != nil {\n\t\tsetup(req)\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\treturn req.Get(fullURL)\n\tcase http.MethodPost:\n\t\treturn req.Post(fullURL)\n\tcase http.MethodPut:\n\t\treturn req.Put(fullURL)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported method: %s\", method)\n\t}\n}\n"
  },
  {
    "path": "drivers/123_open/driver.go",
    "content": "package _123Open\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype Open123 struct {\n\tmodel.Storage\n\tAddition\n\n\tUploadThread int\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\td.tm = newTokenManager(d.ClientID, d.ClientSecret)\n\n\tif _, err := d.tm.getToken(); err != nil {\n\t\treturn fmt.Errorf(\"token 初始化失败: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tparentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfileLastId := int64(0)\n\tvar results []File\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\tfor _, f := range files.Data.FileList {\n\t\t\tif f.Trashed == 0 {\n\t\t\t\tresults = append(results, f)\n\t\t\t}\n\t\t}\n\t\tfileLastId = files.Data.LastFileId\n\t}\n\n\tobjs := make([]model.Obj, 0, len(results))\n\tfor _, f := range results {\n\t\tobjs = append(objs, f)\n\t}\n\treturn objs, nil\n}\n\nfunc (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.IsDir() {\n\t\treturn nil, errs.LinkIsDir\n\t}\n\n\tfileID := file.GetID()\n\n\tvar result DirectLinkResp\n\turl := fmt.Sprintf(\"%s?fileID=%s\", ApiGetDirectLink, fileID)\n\t_, err := d.Request(url, http.MethodGet, nil, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"get link failed: %s\", result.Message)\n\t}\n\n\tlinkURL := result.Data.URL\n\tif d.PrivateKey != \"\" {\n\t\tif d.UID == 0 {\n\t\t\treturn nil, fmt.Errorf(\"uid is required when private key is set\")\n\t\t}\n\t\tduration := time.Duration(d.ValidDuration)\n\t\tif duration <= 0 {\n\t\t\tduration = 30\n\t\t}\n\t\tsignedURL, err := SignURL(linkURL, d.PrivateKey, d.UID, duration*time.Minute)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlinkURL = signedURL\n\t}\n\n\treturn &model.Link{\n\t\tURL: linkURL,\n\t}, nil\n}\n\nfunc (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tparentID, err := strconv.ParseInt(parentDir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid parent ID: %w\", err)\n\t}\n\n\tvar result MakeDirResp\n\treqBody := MakeDirRequest{\n\t\tName:     dirName,\n\t\tParentID: parentID,\n\t}\n\n\t_, err = d.Request(ApiMakeDir, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetBody(reqBody)\n\t}, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"mkdir failed: %s\", result.Message)\n\t}\n\n\tnewDir := File{\n\t\tFileId:       result.Data.DirID,\n\t\tFileName:     dirName,\n\t\tType:         1,\n\t\tParentFileId: int(parentID),\n\t\tSize:         0,\n\t\tTrashed:      0,\n\t}\n\treturn newDir, nil\n}\n\nfunc (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tsrcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid src file ID: %w\", err)\n\t}\n\tdstID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid dest dir ID: %w\", err)\n\t}\n\n\tvar result Response[any]\n\treqBody := map[string]interface{}{\n\t\t\"fileIDs\":        []int64{srcID},\n\t\t\"toParentFileID\": dstID,\n\t}\n\n\t_, err = d.Request(ApiMove, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetBody(reqBody)\n\t}, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"move failed: %s\", result.Message)\n\t}\n\n\tfiles, err := d.getFiles(dstID, 100, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"move succeed but failed to get target dir: %w\", err)\n\t}\n\tfor _, f := range files.Data.FileList {\n\t\tif f.FileId == srcID {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"move succeed but file not found in target dir\")\n}\n\nfunc (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tsrcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid file ID: %w\", err)\n\t}\n\n\tvar result Response[any]\n\treqBody := map[string]interface{}{\n\t\t\"fileId\":   srcID,\n\t\t\"fileName\": newName,\n\t}\n\n\t_, err = d.Request(ApiRename, http.MethodPut, func(r *resty.Request) {\n\t\tr.SetBody(reqBody)\n\t}, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"rename failed: %s\", result.Message)\n\t}\n\n\tparentID := 0\n\tif file, ok := srcObj.(File); ok {\n\t\tparentID = file.ParentFileId\n\t}\n\tfiles, err := d.getFiles(int64(parentID), 100, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rename succeed but failed to get parent dir: %w\", err)\n\t}\n\tfor _, f := range files.Data.FileList {\n\t\tif f.FileId == srcID {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"rename succeed but file not found in parent dir\")\n}\n\nfunc (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *Open123) Remove(ctx context.Context, obj model.Obj) error {\n\tidStr := obj.GetID()\n\tid, err := strconv.ParseInt(idStr, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid file ID: %w\", err)\n\t}\n\n\tvar result Response[any]\n\treqBody := RemoveRequest{\n\t\tFileIDs: []int64{id},\n\t}\n\n\t_, err = d.Request(ApiRemove, http.MethodPost, func(r *resty.Request) {\n\t\tr.SetBody(reqBody)\n\t}, &result)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif result.Code != 0 {\n\t\treturn fmt.Errorf(\"remove failed: %s\", result.Message)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tparentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)\n\tetag := file.GetHash().GetHash(utils.MD5)\n\n\tif len(etag) < utils.MD5.Width {\n\t\tup = model.UpdateProgressWithRange(up, 50, 100)\n\t\t_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)\n\t\tif err != nil {\n\t\t\treturn 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 err\n\t}\n\tif createResp.Data.Reuse {\n\t\treturn nil\n\t}\n\n\treturn d.Upload(ctx, file, parentFileId, createResp, up)\n}\n\nfunc (d *Open123) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *Open123) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *Open123) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\treturn nil, errs.NotSupport\n}\n\nfunc (d *Open123) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotSupport\n}\n\n//func (d *Open123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Open123)(nil)\n"
  },
  {
    "path": "drivers/123_open/meta.go",
    "content": "package _123Open\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\n\tClientID      string `json:\"client_id\" required:\"true\" label:\"Client ID\"`\n\tClientSecret  string `json:\"client_secret\" required:\"true\" label:\"Client Secret\"`\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:              \"123 Open\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Open123{}\n\t})\n}\n"
  },
  {
    "path": "drivers/123_open/sign.go",
    "content": "package _123Open\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) (string, error) {\n\tif privateKey == \"\" {\n\t\treturn originURL, nil\n\t}\n\tparsed, err := url.Parse(originURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tts := time.Now().Add(validDuration).Unix()\n\trandInt := rand.Int()\n\tsignature := fmt.Sprintf(\"%d-%d-%d-%x\", ts, randInt, uid, md5.Sum([]byte(fmt.Sprintf(\"%s-%d-%d-%d-%s\",\n\t\tparsed.Path, ts, randInt, uid, privateKey))))\n\tquery := parsed.Query()\n\tquery.Add(\"auth_key\", signature)\n\tparsed.RawQuery = query.Encode()\n\treturn parsed.String(), nil\n}\n"
  },
  {
    "path": "drivers/123_open/token.go",
    "content": "package _123Open\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst tokenURL = ApiBaseURL + ApiToken\n\ntype tokenManager struct {\n\tclientID     string\n\tclientSecret string\n\n\tmu          sync.Mutex\n\taccessToken string\n\texpireTime  time.Time\n}\n\nfunc newTokenManager(clientID, clientSecret string) *tokenManager {\n\treturn &tokenManager{\n\t\tclientID:     clientID,\n\t\tclientSecret: clientSecret,\n\t}\n}\n\nfunc (tm *tokenManager) getToken() (string, error) {\n\ttm.mu.Lock()\n\tdefer tm.mu.Unlock()\n\n\tif tm.accessToken != \"\" && time.Now().Before(tm.expireTime.Add(-5*time.Minute)) {\n\t\treturn tm.accessToken, nil\n\t}\n\n\treqBody := map[string]string{\n\t\t\"clientID\":     tm.clientID,\n\t\t\"clientSecret\": tm.clientSecret,\n\t}\n\tbody, _ := json.Marshal(reqBody)\n\treq, err := http.NewRequest(\"POST\", tokenURL, bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Platform\", \"open_platform\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result TokenResp\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn \"\", fmt.Errorf(\"get token failed: %s\", result.Message)\n\t}\n\n\ttm.accessToken = result.Data.AccessToken\n\texpireAt, err := time.Parse(time.RFC3339, result.Data.ExpiredAt)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"parse expire time failed: %w\", err)\n\t}\n\ttm.expireTime = expireAt\n\n\treturn tm.accessToken, nil\n}\n\nfunc (tm *tokenManager) buildHeaders() (http.Header, error) {\n\ttoken, err := tm.getToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\theader := http.Header{}\n\theader.Set(\"Authorization\", \"Bearer \"+token)\n\theader.Set(\"Platform\", \"open_platform\")\n\theader.Set(\"Content-Type\", \"application/json\")\n\treturn header, nil\n}\n"
  },
  {
    "path": "drivers/123_open/types.go",
    "content": "package _123Open\n\nimport (\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"time\"\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}\n\nfunc (f File) GetID() string {\n\treturn fmt.Sprint(f.FileId)\n}\n\nfunc (f File) GetName() string {\n\treturn f.FileName\n}\n\nfunc (f File) GetSize() int64 {\n\treturn f.Size\n}\n\nfunc (f File) IsDir() bool {\n\treturn f.Type == 1\n}\n\nfunc (f File) GetModified() string {\n\treturn f.UpdateAt\n}\n\nfunc (f File) GetThumb() string {\n\treturn \"\"\n}\n\nfunc (f File) ModTime() time.Time {\n\tt, err := time.Parse(\"2006-01-02 15:04:05\", f.UpdateAt)\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\treturn t\n}\n\nfunc (f File) CreateTime() time.Time {\n\tt, err := time.Parse(\"2006-01-02 15:04:05\", f.CreateAt)\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\treturn t\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"
  },
  {
    "path": "drivers/123_open/upload.go",
    "content": "package _123Open\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n)\n\nfunc (d *Open123) create(parentFileID int64, filename, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {\n\tvar resp UploadCreateResp\n\n\t_, err := d.Request(ApiCreateUploadURL, http.MethodPost, func(req *resty.Request) {\n\t\tbody := base.Json{\n\t\t\t\"parentFileID\": parentFileID,\n\t\t\t\"filename\":     filename,\n\t\t\t\"etag\":         etag,\n\t\t\t\"size\":         size,\n\t\t}\n\t\tif duplicate > 0 {\n\t\t\tbody[\"duplicate\"] = duplicate\n\t\t}\n\t\tif containDir {\n\t\t\tbody[\"containDir\"] = true\n\t\t}\n\t\treq.SetBody(body)\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Open123) GetUploadDomains() ([]string, error) {\n\tvar resp struct {\n\t\tCode    int      `json:\"code\"`\n\t\tMessage string   `json:\"message\"`\n\t\tData    []string `json:\"data\"`\n\t}\n\n\t_, err := d.Request(ApiUploadDomainURL, http.MethodGet, nil, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"get upload domain failed: %s\", resp.Message)\n\t}\n\treturn resp.Data, nil\n}\n\nfunc (d *Open123) UploadSingle(ctx context.Context, createResp *UploadCreateResp, file model.FileStreamer, parentID int64) error {\n\tdomain := createResp.Data.Servers[0]\n\n\tetag := file.GetHash().GetHash(utils.MD5)\n\tif len(etag) < utils.MD5.Width {\n\t\t_, _, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treader, err := file.RangeRead(http_range.Range{Start: 0, Length: file.GetSize()})\n\tif err != nil {\n\t\treturn err\n\t}\n\treader = driver.NewLimitedUploadStream(ctx, reader)\n\n\tvar b bytes.Buffer\n\tmw := multipart.NewWriter(&b)\n\tmw.WriteField(\"parentFileID\", fmt.Sprint(parentID))\n\tmw.WriteField(\"filename\", file.GetName())\n\tmw.WriteField(\"etag\", etag)\n\tmw.WriteField(\"size\", fmt.Sprint(file.GetSize()))\n\tfw, _ := mw.CreateFormFile(\"file\", file.GetName())\n\t_, err = io.Copy(fw, reader)\n\tmw.Close()\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", domain+ApiSingleUploadURL, &b)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.tm.accessToken)\n\treq.Header.Set(\"Platform\", \"open_platform\")\n\treq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData    struct {\n\t\t\tFileID    int64 `json:\"fileID\"`\n\t\t\tCompleted bool  `json:\"completed\"`\n\t\t} `json:\"data\"`\n\t}\n\tbody, _ := io.ReadAll(resp.Body)\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal response error: %v, body: %s\", err, string(body))\n\t}\n\tif result.Code != 0 {\n\t\treturn fmt.Errorf(\"upload failed: %s\", result.Message)\n\t}\n\tif !result.Data.Completed || result.Data.FileID == 0 {\n\t\treturn fmt.Errorf(\"upload incomplete or missing fileID\")\n\t}\n\treturn nil\n}\n\nfunc (d *Open123) Upload(ctx context.Context, file model.FileStreamer, parentID int64, createResp *UploadCreateResp, up driver.UpdateProgress) error {\n\tif cacher, ok := file.(interface{ CacheFullInTempFile() (model.File, error) }); ok {\n\t\tif _, err := cacher.CacheFullInTempFile(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tsize := file.GetSize()\n\tchunkSize := createResp.Data.SliceSize\n\tuploadNums := (size + chunkSize - 1) / chunkSize\n\tuploadDomain := createResp.Data.Servers[0]\n\n\tif d.UploadThread <= 0 {\n\t\tcpuCores := runtime.NumCPU()\n\t\tthreads := cpuCores * 2\n\t\tif threads < 4 {\n\t\t\tthreads = 4\n\t\t}\n\t\tif threads > 16 {\n\t\t\tthreads = 16\n\t\t}\n\t\td.UploadThread = threads\n\t\tfmt.Printf(\"[Upload] Auto set upload concurrency: %d (CPU cores=%d)\\n\", d.UploadThread, cpuCores)\n\t}\n\n\tfmt.Printf(\"[Upload] File size: %d bytes, chunk size: %d bytes, total slices: %d, concurrency: %d\\n\",\n\t\tsize, chunkSize, uploadNums, d.UploadThread)\n\n\tif size <= 1<<30 {\n\t\treturn d.UploadSingle(ctx, createResp, file, parentID)\n\t}\n\n\tif createResp.Data.Reuse {\n\t\tup(100)\n\t\treturn nil\n\t}\n\n\tclient := resty.New()\n\tsemaphore := make(chan struct{}, d.UploadThread)\n\tthreadG, _ := errgroup.WithContext(ctx)\n\n\tvar progressArr = make([]int64, uploadNums)\n\n\tfor partIndex := int64(0); partIndex < uploadNums; partIndex++ {\n\t\tpartIndex := partIndex\n\t\tsemaphore <- struct{}{}\n\n\t\tthreadG.Go(func() error {\n\t\t\tdefer func() { <-semaphore }()\n\t\t\toffset := partIndex * chunkSize\n\t\t\tlength := min(chunkSize, size-offset)\n\t\t\tpartNumber := partIndex + 1\n\n\t\t\tfmt.Printf(\"[Slice %d] Starting read from offset %d, length %d\\n\", partNumber, offset, length)\n\t\t\treader, err := file.RangeRead(http_range.Range{Start: offset, Length: length})\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"[Slice %d] RangeRead error: %v\", partNumber, err)\n\t\t\t}\n\n\t\t\tbuf := make([]byte, length)\n\t\t\tn, err := io.ReadFull(reader, buf)\n\t\t\tif err != nil && err != io.EOF {\n\t\t\t\treturn fmt.Errorf(\"[Slice %d] Read error: %v\", partNumber, err)\n\t\t\t}\n\t\t\tbuf = buf[:n]\n\t\t\thash := md5.Sum(buf)\n\t\t\tsliceMD5Str := hex.EncodeToString(hash[:])\n\n\t\t\tbody := &bytes.Buffer{}\n\t\t\twriter := multipart.NewWriter(body)\n\t\t\twriter.WriteField(\"preuploadID\", createResp.Data.PreuploadID)\n\t\t\twriter.WriteField(\"sliceNo\", strconv.FormatInt(partNumber, 10))\n\t\t\twriter.WriteField(\"sliceMD5\", sliceMD5Str)\n\t\t\tpartName := fmt.Sprintf(\"%s.part%d\", file.GetName(), partNumber)\n\t\t\tfw, _ := writer.CreateFormFile(\"slice\", partName)\n\t\t\tfw.Write(buf)\n\t\t\twriter.Close()\n\n\t\t\tresp, err := client.R().\n\t\t\t\tSetHeader(\"Authorization\", \"Bearer \"+d.tm.accessToken).\n\t\t\t\tSetHeader(\"Platform\", \"open_platform\").\n\t\t\t\tSetHeader(\"Content-Type\", writer.FormDataContentType()).\n\t\t\t\tSetBody(body.Bytes()).\n\t\t\t\tPost(uploadDomain + ApiUploadSliceURL)\n\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"[Slice %d] Upload HTTP error: %v\", partNumber, err)\n\t\t\t}\n\t\t\tif resp.StatusCode() != 200 {\n\t\t\t\treturn fmt.Errorf(\"[Slice %d] Upload failed with status: %s, resp: %s\", partNumber, resp.Status(), resp.String())\n\t\t\t}\n\n\t\t\tprogressArr[partIndex] = length\n\t\t\tvar totalUploaded int64 = 0\n\t\t\tfor _, v := range progressArr {\n\t\t\t\ttotalUploaded += v\n\t\t\t}\n\t\t\tif up != nil {\n\t\t\t\tpercent := float64(totalUploaded) / float64(size) * 100\n\t\t\t\tup(percent)\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[Slice %d] MD5: %s\\n\", partNumber, sliceMD5Str)\n\t\t\tfmt.Printf(\"[Slice %d] Upload finished\\n\", partNumber)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := threadG.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tvar completeResp struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tData    struct {\n\t\t\tCompleted bool  `json:\"completed\"`\n\t\t\tFileID    int64 `json:\"fileID\"`\n\t\t} `json:\"data\"`\n\t}\n\n\tfor {\n\t\treqBody := fmt.Sprintf(`{\"preuploadID\":\"%s\"}`, createResp.Data.PreuploadID)\n\t\treq, err := http.NewRequestWithContext(ctx, \"POST\", uploadDomain+ApiUploadCompleteURL, bytes.NewBufferString(reqBody))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+d.tm.accessToken)\n\t\treq.Header.Set(\"Platform\", \"open_platform\")\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\n\t\tif err := json.Unmarshal(body, &completeResp); err != nil {\n\t\t\treturn fmt.Errorf(\"completion response unmarshal error: %v, body: %s\", err, string(body))\n\t\t}\n\t\tif completeResp.Code != 0 {\n\t\t\treturn fmt.Errorf(\"completion API returned error code %d: %s\", completeResp.Code, completeResp.Message)\n\t\t}\n\t\tif completeResp.Data.Completed && completeResp.Data.FileID != 0 {\n\t\t\tfmt.Printf(\"[Upload] Upload completed successfully. FileID: %d\\n\", completeResp.Data.FileID)\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n\tup(100)\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/123_open/util.go",
    "content": "package _123Open\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {\n\tvar result FileListResp\n\turl := fmt.Sprintf(\"%s?parentFileId=%d&limit=%d&lastFileId=%d\", ApiFileList, parentFileId, limit, lastFileId)\n\n\t_, err := d.Request(url, http.MethodGet, nil, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"list error: %s\", result.Message)\n\t}\n\treturn &result, 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/alist-org/alist/v3/drivers/123\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\t//var resp DownResp\n\t\tvar headers map[string]string\n\t\tif !utils.IsLocalIPAddr(args.IP) {\n\t\t\theaders = map[string]string{\n\t\t\t\t//\"X-Real-IP\":       \"1.1.1.1\",\n\t\t\t\t\"X-Forwarded-For\": args.IP,\n\t\t\t}\n\t\t}\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).SetHeaders(headers)\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\tu, err := url.Parse(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnu := u.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}\n\t\tu_ := u.String()\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{\"https://www.123pan.com/\"},\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          true,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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.HashInfo{}\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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) alist-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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\tstreamPkg \"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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}\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\treturn fmt.Errorf(\"authorization is empty\")\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\"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\tcase MetaFamily:\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\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\t// 网页接口不支持重命名家庭云文件夹\n\t\t\t// data = base.Json{\n\t\t\t// \t\"catalogType\": 3,\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// \t\"path\": srcObj.GetPath(),\n\t\t\t// }\n\t\t\t// pathname = \"/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo\"\n\t\t\treturn errs.NotImplement\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\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.CacheFullInTempFileAndHash(stream, 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\tvar partSize = d.getPartSize(size)\n\t\tpart := size / partSize\n\t\tif size%partSize > 0 {\n\t\t\tpart++\n\t\t} else if part == 0 {\n\t\t\tpart = 1\n\t\t}\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 := size - start\n\t\t\tif byteSize > partSize {\n\t\t\t\tbyteSize = partSize\n\t\t\t}\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// 读取前100个分片的上传地址\n\t\t\tuploadPartInfos := resp.Data.PartInfos\n\n\t\t\t// 获取后续分片的上传地址\n\t\t\tfor i := 101; i < len(partInfos); i += 100 {\n\t\t\t\tend := i + 100\n\t\t\t\tif end > len(partInfos) {\n\t\t\t\t\tend = len(partInfos)\n\t\t\t\t}\n\t\t\t\tbatchPartInfos := partInfos[i:end]\n\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\tuploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)\n\t\t\t}\n\n\t\t\t// Progress\n\t\t\tp := driver.NewProgress(size, up)\n\n\t\t\trateLimited := driver.NewLimitedUploadStream(ctx, stream)\n\t\t\t// 上传所有分片\n\t\t\tfor _, uploadPartInfo := range uploadPartInfos {\n\t\t\t\tindex := uploadPartInfo.PartNumber - 1\n\t\t\t\tpartSize := partInfos[index].PartSize\n\t\t\t\tlog.Debugf(\"[139] uploading part %+v/%+v\", index, len(uploadPartInfos))\n\t\t\t\tlimitReader := io.LimitReader(rateLimited, partSize)\n\n\t\t\t\t// Update Progress\n\t\t\t\tr := io.TeeReader(limitReader, p)\n\n\t\t\t\treq, err := http.NewRequest(\"PUT\", uploadPartInfo.UploadUrl, r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq = req.WithContext(ctx)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\t\t\t\treq.Header.Set(\"Content-Length\", fmt.Sprint(partSize))\n\t\t\t\treq.Header.Set(\"Origin\", \"https://yun.139.com\")\n\t\t\t\treq.Header.Set(\"Referer\", \"https://yun.139.com/\")\n\t\t\t\treq.ContentLength = partSize\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\t_ = res.Body.Close()\n\t\t\t\tlog.Debugf(\"[139] uploaded: %+v\", res)\n\t\t\t\tif res.StatusCode != http.StatusOK {\n\t\t\t\t\treturn fmt.Errorf(\"unexpected status code: %d\", res.StatusCode)\n\t\t\t\t}\n\t\t\t}\n\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 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() {\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\":         path.Join(dstDir.GetPath(), dstDir.GetID()),\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\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\tvar partSize = d.getPartSize(size)\n\t\tpart := size / partSize\n\t\tif size%partSize > 0 {\n\t\t\tpart++\n\t\t} else if part == 0 {\n\t\t\tpart = 1\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.NewRequest(\"POST\", resp.Data.UploadResult.RedirectionURL, r)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treq = req.WithContext(ctx)\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\nvar _ driver.Driver = (*Yun139)(nil)\n"
  },
  {
    "path": "drivers/139/meta.go",
    "content": "package _139\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\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\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"
  },
  {
    "path": "drivers/139/util.go",
    "content": "package _139\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\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\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 {\n\t\treturn err\n\t}\n\tif resp.Return != \"0\" {\n\t\treturn fmt.Errorf(\"failed to refresh token: %s\", resp.Desc)\n\t}\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(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\turl := \"https://yun.139.com\" + 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\"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(method, 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) 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(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\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\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}\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\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.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}\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 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\tvar isFolder = (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\tvar Thumbnails = 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\tvar cdnUrl = 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}\nfunc (d *Yun139) getAccount() string {\n\tif d.ref != nil {\n\t\treturn d.ref.getAccount()\n\t}\n\treturn d.Account\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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tsafeName := d.sanitizeName(dirName)\n\tform := map[string]string{\n\t\t\"parentFolderId\": parentDir.GetID(),\n\t\t\"folderName\":     safeName,\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\tsafeName := d.sanitizeName(newName)\n\tform := map[string]string{\n\t\tidKey:   srcObj.GetID(),\n\t\tnameKey: safeName,\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\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/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tStripEmoji bool   `json:\"strip_emoji\" help:\"Remove four-byte characters (e.g., emoji) before upload\"`\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"
  },
  {
    "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\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tmyrand \"github.com/alist-org/alist/v3/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\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36\",\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) sanitizeName(name string) string {\n\tif !d.StripEmoji {\n\t\treturn name\n\t}\n\tb := strings.Builder{}\n\tfor _, r := range name {\n\t\tif utf8.RuneLen(r) == 4 {\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteRune(r)\n\t}\n\tsanitized := b.String()\n\tif sanitized == \"\" {\n\t\text := path.Ext(name)\n\t\tif ext != \"\" {\n\t\t\tsanitized = \"file\" + ext\n\t\t} else {\n\t\t\tsanitized = \"file\"\n\t\t}\n\t}\n\treturn sanitized\n}\n\nfunc (d *Cloud189) oldUpload(dstDir model.Obj, file model.FileStreamer) error {\n\tsafeName := d.sanitizeName(file.GetName())\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\":      safeName,\n\t}).SetMultipartField(\"Filedata\", safeName, 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\tvar count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))\n\n\tsafeName := d.sanitizeName(file.GetName())\n\tres, err := d.uploadRequest(\"/person/initMultiUpload\", map[string]string{\n\t\t\"parentFolderId\": dstDir.GetID(),\n\t\t\"fileName\":       encode(safeName),\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.NewRequest(http.MethodPut, requestURL, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tidentity string\n\n\tclient *resty.Client\n\n\tloginParam *LoginParam\n\ttokenInfo  *AppSessionResp\n\n\tuploadThread int\n\n\tfamilyTransferFolder    *Cloud189Folder\n\tcleanFamilyTransferFile func()\n\n\tstorageConfig driver.Config\n\tref           *Cloud189PC\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// 避免重复登陆\n\t\tidentity := utils.GetMD5EncodeStr(y.Username + y.Password)\n\t\tif !y.isLogin() || y.identity != identity {\n\t\t\ty.identity = identity\n\t\t\tif err = y.login(); err != nil {\n\t\t\t\treturn\n\t\t\t}\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\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\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\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\tsafeName := y.sanitizeName(dirName)\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\":   safeName,\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\tnewFolder.Name = safeName\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\tsafeName := y.sanitizeName(newName)\n\tswitch f := srcObj.(type) {\n\tcase *Cloud189File:\n\t\tfullUrl += \"/renameFile.action\"\n\t\tqueryParam[\"fileId\"] = srcObj.GetID()\n\t\tqueryParam[\"destFileName\"] = safeName\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\"] = safeName\n\t\tnewObj = &Cloud189Folder{}\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n\n\tswitch obj := newObj.(type) {\n\tcase *Cloud189File:\n\t\tobj.Name = safeName\n\tcase *Cloud189Folder:\n\t\tobj.Name = safeName\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\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"
  },
  {
    "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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tUsername   string `json:\"username\" required:\"true\"`\n\tPassword   string `json:\"password\" required:\"true\"`\n\tVCode      string `json:\"validate_code\"`\n\tStripEmoji bool   `json:\"strip_emoji\" help:\"Remove four-byte characters (e.g., emoji) before upload\"`\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/alist-org/alist/v3/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// 登陆需要的参数\ntype LoginParam struct {\n\t// 加密后的用户名和密码\n\tRsaUsername string\n\tRsaPassword string\n\n\t// rsa密钥\n\tjRsaKey string\n\n\t// 请求头参数\n\tLt    string\n\tReqId string\n\n\t// 表单参数\n\tParamId string\n\n\t// 验证码\n\tCaptchaToken string\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"
  },
  {
    "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\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\nfunc (y *Cloud189PC) sanitizeName(name string) string {\n\tif !y.StripEmoji {\n\t\treturn name\n\t}\n\tb := strings.Builder{}\n\tfor _, r := range name {\n\t\tif utf8.RuneLen(r) == 4 {\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteRune(r)\n\t}\n\tsanitized := b.String()\n\tif sanitized == \"\" {\n\t\text := path.Ext(name)\n\t\tif ext != \"\" {\n\t\t\tsanitized = \"file\" + ext\n\t\t} else {\n\t\t\tsanitized = \"file\"\n\t\t}\n\t}\n\treturn sanitized\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\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}\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() (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 && y.NoUseOcr {\n\t\t\tif err1 := y.initLoginParam(); err1 != nil {\n\t\t\t\terr = fmt.Errorf(\"err1: %s \\nerr2: %s\", err, err1)\n\t\t\t}\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\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\n\t}\n\ty.tokenInfo = &tokenInfo\n\treturn\n}\n\n/* 初始化登陆需要的参数\n*  如果遇到验证码返回错误\n */\nfunc (y *Cloud189PC) initLoginParam() 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 err\n\t}\n\n\tparam := LoginParam{\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\t// jRsaKey:      regexp.MustCompile(`\"j_rsaKey\" value=\"(.+?)\"`).FindStringSubmatch(res.String())[1],\n\t}\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\tparam.jRsaKey = fmt.Sprintf(\"-----BEGIN PUBLIC KEY-----\\n%s\\n-----END PUBLIC KEY-----\", encryptConf.Data.PubKey)\n\tparam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)\n\tparam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)\n\ty.loginParam = &param\n\n\t// 判断是否需要验证码\n\tresp, err := y.client.R().\n\t\tSetHeader(\"REQID\", param.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\":    param.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\": param.CaptchaToken,\n\t\t\t\"REQID\": param.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// 刷新会话\nfunc (y *Cloud189PC) refreshSession() (err error) {\n\tif y.ref != nil {\n\t\treturn y.ref.refreshSession()\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// 错误影响正常访问，下线该储存\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ty.GetStorage().SetStatus(fmt.Sprintf(\"%+v\", err.Error()))\n\t\t\top.MustSaveDriverStorage(y)\n\t\t}\n\t}()\n\n\tif erron.HasError() {\n\t\tif erron.ResCode == \"UserInvalidOpenToken\" {\n\t\t\tif err = y.login(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn &erron\n\t}\n\ty.tokenInfo.UserSessionResp = userSessionResp\n\treturn\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\tsize := file.GetSize()\n\tsliceSize := partSize(size)\n\tsafeName := y.sanitizeName(file.GetName())\n\n\tparams := Params{\n\t\t\"parentFolderId\": dstDir.GetID(),\n\t\t\"fileName\":       url.QueryEscape(safeName),\n\t\t\"fileSize\":       fmt.Sprint(file.GetSize()),\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\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, y.uploadThread,\n\t\tretry.Attempts(3),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\tsem := semaphore.NewWeighted(3)\n\n\tcount := int(size / sliceSize)\n\tlastPartSize := size % sliceSize\n\tif lastPartSize > 0 {\n\t\tcount++\n\t} else {\n\t\tlastPartSize = sliceSize\n\t}\n\tfileMd5 := utils.MD5.NewFunc()\n\tsilceMd5 := utils.MD5.NewFunc()\n\tsilceMd5Hexs := make([]string, 0, count)\n\tteeReader := io.TeeReader(file, io.MultiWriter(fileMd5, silceMd5))\n\tbyteSize := sliceSize\n\tfor i := 1; i <= count; i++ {\n\t\tif utils.IsCanceled(upCtx) {\n\t\t\tbreak\n\t\t}\n\t\tif i == count {\n\t\t\tbyteSize = lastPartSize\n\t\t}\n\t\tbyteData := make([]byte, byteSize)\n\t\t// 读取块\n\t\tsilceMd5.Reset()\n\t\tif _, err := io.ReadFull(teeReader, byteData); err != io.EOF && err != nil {\n\t\t\tsem.Release(1)\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 计算块md5并进行hex和base64编码\n\t\tmd5Bytes := silceMd5.Sum(nil)\n\t\tsilceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))\n\t\tpartInfo := fmt.Sprintf(\"%d-%s\", i, base64.StdEncoding.EncodeToString(md5Bytes))\n\n\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\tif err = sem.Acquire(ctx, 1); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer sem.Release(1)\n\t\t\tuploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// step.4 上传切片\n\t\t\tuploadUrl := uploadUrls[0]\n\t\t\t_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false,\n\t\t\t\tdriver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)), isFamily)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tup(float64(threadG.Success()) * 100 / float64(count))\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err = threadG.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))\n\tsliceMd5Hex := fileMd5Hex\n\tif file.GetSize() > 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\tsafeName := y.sanitizeName(stream.GetName())\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, safeName, 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\tsafeName := y.sanitizeName(file.GetName())\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 := int(size / sliceSize)\n\tlastSliceSize := size % sliceSize\n\tif lastSliceSize > 0 {\n\t\tcount++\n\t} else {\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(safeName),\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\t_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(cache, offset, byteSize), 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()) * 100 / float64(len(uploadUrls)))\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}\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.CacheFullInTempFileAndHash(file, utils.MD5)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trateLimited := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))\n\tsafeName := y.sanitizeName(file.GetName())\n\n\t// 创建上传会话\n\tuploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, safeName, 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\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"
  },
  {
    "path": "drivers/alias/driver.go",
    "content": "package alias\n\nimport (\n\t\"context\"\n\t\"errors\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype Alias struct {\n\tmodel.Storage\n\tAddition\n\tpathMap     map[string][]string\n\tautoFlatten bool\n\toneKey      string\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\tif d.Paths == \"\" {\n\t\treturn errors.New(\"paths 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}\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\treturn nil\n}\n\nfunc (d *Alias) Drop(ctx context.Context) error {\n\td.pathMap = nil\n\treturn nil\n}\n\nfunc (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {\n\tif utils.PathEqual(path, \"/\") {\n\t\treturn &model.Object{\n\t\t\tName:     \"Root\",\n\t\t\tIsFolder: true,\n\t\t\tPath:     \"/\",\n\t\t}, 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\tfor _, dst := range dsts {\n\t\tobj, err := d.get(ctx, path, dst, sub)\n\t\tif err == nil {\n\t\t\treturn obj, nil\n\t\t}\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\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 *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\troot, sub := d.getRootAndPath(file.GetPath())\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor _, dst := range dsts {\n\t\tlink, err := d.link(ctx, dst, sub, args)\n\t\tif err == nil {\n\t\t\tif !args.Redirect && len(link.URL) > 0 {\n\t\t\t\t// 正常情况下 多并发 仅支持返回URL的驱动\n\t\t\t\t// alias套娃alias 可以让crypt、mega等驱动(不返回URL的) 支持并发\n\t\t\t\tif d.DownloadConcurrency > 0 {\n\t\t\t\t\tlink.Concurrency = d.DownloadConcurrency\n\t\t\t\t}\n\t\t\t\tif d.DownloadPartSize > 0 {\n\t\t\t\t\tlink.PartSize = d.DownloadPartSize * utils.KB\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn link, nil\n\t\t}\n\t}\n\treturn nil, errs.ObjectNotFound\n}\n\nfunc (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := d.getReqPath(ctx, parentDir, true)\n\tif err == nil {\n\t\treturn fs.MakeDir(ctx, stdpath.Join(*reqPath, dirName))\n\t}\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name dirs cannot make sub-dir\")\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\tsrcPath, err := d.getReqPath(ctx, srcObj, false)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot be moved\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstPath, err := d.getReqPath(ctx, dstDir, true)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name dirs cannot be moved to\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn fs.Move(ctx, *srcPath, *dstPath)\n}\n\nfunc (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := d.getReqPath(ctx, srcObj, false)\n\tif err == nil {\n\t\treturn fs.Rename(ctx, *reqPath, newName)\n\t}\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot be Rename\")\n\t}\n\treturn err\n}\n\nfunc (d *Alias) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\tsrcPath, err := d.getReqPath(ctx, srcObj, false)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot be copied\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstPath, err := d.getReqPath(ctx, dstDir, true)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name dirs cannot be copied to\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = fs.Copy(ctx, *srcPath, *dstPath)\n\treturn err\n}\n\nfunc (d *Alias) Remove(ctx context.Context, obj model.Obj) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := d.getReqPath(ctx, obj, false)\n\tif err == nil {\n\t\treturn fs.Remove(ctx, *reqPath)\n\t}\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot be Delete\")\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\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := d.getReqPath(ctx, dstDir, true)\n\tif err == nil {\n\t\treturn fs.PutDirectly(ctx, *reqPath, s)\n\t}\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name dirs cannot be Put\")\n\t}\n\treturn err\n}\n\nfunc (d *Alias) PutURL(ctx context.Context, dstDir model.Obj, name, url string) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := d.getReqPath(ctx, dstDir, true)\n\tif err == nil {\n\t\treturn fs.PutURL(ctx, *reqPath, name, url)\n\t}\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot offline download\")\n\t}\n\treturn err\n}\n\nfunc (d *Alias) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\troot, sub := d.getRootAndPath(obj.GetPath())\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor _, dst := range dsts {\n\t\tmeta, err := d.getArchiveMeta(ctx, dst, sub, args)\n\t\tif err == nil {\n\t\t\treturn meta, nil\n\t\t}\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\troot, sub := d.getRootAndPath(obj.GetPath())\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor _, dst := range dsts {\n\t\tl, err := d.listArchive(ctx, dst, sub, args)\n\t\tif err == nil {\n\t\t\treturn l, nil\n\t\t}\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\troot, sub := d.getRootAndPath(obj.GetPath())\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tfor _, dst := range dsts {\n\t\tlink, err := d.extract(ctx, dst, sub, args)\n\t\tif err == nil {\n\t\t\tif !args.Redirect && len(link.URL) > 0 {\n\t\t\t\tif d.DownloadConcurrency > 0 {\n\t\t\t\t\tlink.Concurrency = d.DownloadConcurrency\n\t\t\t\t}\n\t\t\t\tif d.DownloadPartSize > 0 {\n\t\t\t\t\tlink.PartSize = d.DownloadPartSize * utils.KB\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn link, nil\n\t\t}\n\t}\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {\n\tif !d.Writable {\n\t\treturn errs.PermissionDenied\n\t}\n\tsrcPath, err := d.getReqPath(ctx, srcObj, false)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name files cannot be decompressed\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstPath, err := d.getReqPath(ctx, dstDir, true)\n\tif errs.IsNotImplement(err) {\n\t\treturn errors.New(\"same-name dirs cannot be decompressed to\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = fs.ArchiveDecompress(ctx, *srcPath, *dstPath, args)\n\treturn err\n}\n\nvar _ driver.Driver = (*Alias)(nil)\n"
  },
  {
    "path": "drivers/alias/meta.go",
    "content": "package alias\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\t// driver.RootPath\n\t// define other\n\tPaths               string `json:\"paths\" required:\"true\" type:\"text\"`\n\tProtectSameName     bool   `json:\"protect_same_name\" default:\"true\" required:\"false\" help:\"Protects same-name files from Delete or Rename\"`\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\tWritable            bool   `json:\"writable\" 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}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Alias{\n\t\t\tAddition: Addition{\n\t\t\t\tProtectSameName: true,\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/alias/types.go",
    "content": "package alias\n"
  },
  {
    "path": "drivers/alias/util.go",
    "content": "package alias\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n)\n\nfunc (d *Alias) listRoot() []model.Obj {\n\tvar objs []model.Obj\n\tfor k := range d.pathMap {\n\t\tobj := model.Object{\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 *Alias) 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 *Alias) get(ctx context.Context, path string, dst, sub string) (model.Obj, error) {\n\tobj, err := fs.Get(ctx, stdpath.Join(dst, sub), &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Object{\n\t\tPath:     path,\n\t\tName:     obj.GetName(),\n\t\tSize:     obj.GetSize(),\n\t\tModified: obj.ModTime(),\n\t\tIsFolder: obj.IsDir(),\n\t\tHashInfo: obj.GetHash(),\n\t}, nil\n}\n\nfunc (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {\n\tobjs, err := fs.List(ctx, stdpath.Join(dst, sub), args)\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\treturn utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) {\n\t\tthumb, ok := model.GetThumb(obj)\n\t\tobjRes := model.Object{\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}\n\t\tif !ok {\n\t\t\treturn &objRes, nil\n\t\t}\n\t\treturn &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}, nil\n\t})\n}\n\nfunc (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {\n\treqPath := stdpath.Join(dst, sub)\n\t// 参考 crypt 驱动\n\tstorage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, ok := storage.(*Alias); !ok && !args.Redirect {\n\t\tlink, _, err := op.Link(ctx, storage, reqActualPath, args)\n\t\treturn link, err\n\t}\n\t_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif common.ShouldProxy(storage, stdpath.Base(sub)) {\n\t\tlink := &model.Link{\n\t\t\tURL: fmt.Sprintf(\"%s/p%s?sign=%s\",\n\t\t\t\tcommon.GetApiUrl(args.HttpReq),\n\t\t\t\tutils.EncodePath(reqPath, true),\n\t\t\t\tsign.Sign(reqPath)),\n\t\t}\n\t\tif args.HttpReq != nil && d.ProxyRange {\n\t\t\tlink.RangeReadCloser = common.NoProxyRange\n\t\t}\n\t\treturn link, nil\n\t}\n\tlink, _, err := op.Link(ctx, storage, reqActualPath, args)\n\treturn link, err\n}\n\nfunc (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) (*string, error) {\n\troot, sub := d.getRootAndPath(obj.GetPath())\n\tif sub == \"\" && !isParent {\n\t\treturn nil, errs.NotSupport\n\t}\n\tdsts, ok := d.pathMap[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\tvar reqPath *string\n\tfor _, dst := range dsts {\n\t\tpath := stdpath.Join(dst, sub)\n\t\t_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif !d.ProtectSameName {\n\t\t\treturn &path, nil\n\t\t}\n\t\tif ok {\n\t\t\tok = false\n\t\t} else {\n\t\t\treturn nil, errs.NotImplement\n\t\t}\n\t\treqPath = &path\n\t}\n\tif reqPath == nil {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\treturn reqPath, nil\n}\n\nfunc (d *Alias) getArchiveMeta(ctx context.Context, dst, sub string, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treqPath := stdpath.Join(dst, sub)\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, dst, sub string, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treqPath := stdpath.Join(dst, sub)\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, dst, sub string, args model.ArchiveInnerArgs) (*model.Link, error) {\n\treqPath := stdpath.Join(dst, sub)\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\tif _, ok := storage.(*Alias); !ok && !args.Redirect {\n\t\t\tlink, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)\n\t\t\treturn link, err\n\t\t}\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\tif common.ShouldProxy(storage, stdpath.Base(sub)) {\n\t\t\tlink := &model.Link{\n\t\t\t\tURL: fmt.Sprintf(\"%s/ap%s?inner=%s&pass=%s&sign=%s\",\n\t\t\t\t\tcommon.GetApiUrl(args.HttpReq),\n\t\t\t\t\tutils.EncodePath(reqPath, true),\n\t\t\t\t\tutils.EncodePath(args.InnerPath, true),\n\t\t\t\t\turl.QueryEscape(args.Password),\n\t\t\t\t\tsign.SignArchive(reqPath)),\n\t\t\t}\n\t\t\tif args.HttpReq != nil && d.ProxyRange {\n\t\t\t\tlink.RangeReadCloser = common.NoProxyRange\n\t\t\t}\n\t\t\treturn link, nil\n\t\t}\n\t\tlink, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)\n\t\treturn link, err\n\t}\n\treturn nil, errs.NotImplement\n}\n"
  },
  {
    "path": "drivers/alist_v2/driver.go",
    "content": "package alist_v2\n\nimport (\n\t\"context\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n)\n\ntype AListV2 struct {\n\tmodel.Storage\n\tAddition\n}\n\nfunc (d *AListV2) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *AListV2) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *AListV2) Init(ctx context.Context) error {\n\tif len(d.Addition.Address) > 0 && string(d.Addition.Address[len(d.Addition.Address)-1]) == \"/\" {\n\t\td.Addition.Address = d.Addition.Address[0 : len(d.Addition.Address)-1]\n\t}\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *AListV2) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *AListV2) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\turl := d.Address + \"/api/public/path\"\n\tvar resp common.Resp[PathResp]\n\t_, err := base.RestyClient.R().\n\t\tSetResult(&resp).\n\t\tSetHeader(\"Authorization\", d.AccessToken).\n\t\tSetBody(PathReq{\n\t\t\tPageNum:  0,\n\t\t\tPageSize: 0,\n\t\t\tPath:     dir.GetPath(),\n\t\t\tPassword: d.Password,\n\t\t}).Post(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range resp.Data.Files {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: *f.UpdatedAt,\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.Type == 1,\n\t\t\t},\n\t\t\tThumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},\n\t\t}\n\t\tfiles = append(files, &file)\n\t}\n\treturn files, nil\n}\n\nfunc (d *AListV2) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\turl := d.Address + \"/api/public/path\"\n\tvar resp common.Resp[PathResp]\n\t_, err := base.RestyClient.R().\n\t\tSetResult(&resp).\n\t\tSetHeader(\"Authorization\", d.AccessToken).\n\t\tSetBody(PathReq{\n\t\t\tPageNum:  0,\n\t\t\tPageSize: 0,\n\t\t\tPath:     file.GetPath(),\n\t\t\tPassword: d.Password,\n\t\t}).Post(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: resp.Data.Files[0].Url,\n\t}, nil\n}\n\nfunc (d *AListV2) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *AListV2) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *AListV2) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *AListV2) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *AListV2) Remove(ctx context.Context, obj model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *AListV2) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\treturn errs.NotImplement\n}\n\n//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*AListV2)(nil)\n"
  },
  {
    "path": "drivers/alist_v2/meta.go",
    "content": "package alist_v2\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tAddress     string `json:\"url\" required:\"true\"`\n\tPassword    string `json:\"password\"`\n\tAccessToken string `json:\"access_token\"`\n}\n\nvar config = driver.Config{\n\tName:        \"AList V2\",\n\tLocalSort:   true,\n\tNoUpload:    true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &AListV2{}\n\t})\n}\n"
  },
  {
    "path": "drivers/alist_v2/types.go",
    "content": "package alist_v2\n\nimport (\n\t\"time\"\n)\n\ntype File struct {\n\tId        string     `json:\"-\"`\n\tName      string     `json:\"name\"`\n\tSize      int64      `json:\"size\"`\n\tType      int        `json:\"type\"`\n\tDriver    string     `json:\"driver\"`\n\tUpdatedAt *time.Time `json:\"updated_at\"`\n\tThumbnail string     `json:\"thumbnail\"`\n\tUrl       string     `json:\"url\"`\n\tSizeStr   string     `json:\"size_str\"`\n\tTimeStr   string     `json:\"time_str\"`\n}\n\ntype PathResp struct {\n\tType string `json:\"type\"`\n\t//Meta  Meta         `json:\"meta\"`\n\tFiles []File `json:\"files\"`\n}\n\ntype PathReq struct {\n\tPageNum  int    `json:\"page_num\"`\n\tPageSize int    `json:\"page_size\"`\n\tPassword string `json:\"password\"`\n\tPath     string `json:\"path\"`\n}\n"
  },
  {
    "path": "drivers/alist_v2/util.go",
    "content": "package alist_v2\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\t// if PassUAToUpsteam is true, then pass the user-agent to the upstream\n\tuserAgent := base.UserAgent\n\tif d.PassUAToUpsteam {\n\t\tuserAgent = args.Header.Get(\"user-agent\")\n\t\tif userAgent == \"\" {\n\t\t\tuserAgent = base.UserAgent\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}).SetHeader(\"user-agent\", userAgent)\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(\"[alist_v3] 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\n//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*AListV3)(nil)\n"
  },
  {
    "path": "drivers/alist_v3/meta.go",
    "content": "package alist_v3\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\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\tCheckStatus:      true,\n\tProxyRangeOption: true,\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 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\ntype IntSlice []int\n\nfunc (s *IntSlice) UnmarshalJSON(data []byte) error {\n\tif len(data) > 0 && data[0] == '[' {\n\t\treturn json.Unmarshal(data, (*[]int)(s))\n\t}\n\tvar single int\n\tif err := json.Unmarshal(data, &single); err != nil {\n\t\treturn err\n\t}\n\t*s = []int{single}\n\treturn nil\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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(\"[alist_v3] 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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/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 = d.Addition.DeviceID\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\tvar count = 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.NewRequest(\"PUT\", url, io.LimitReader(rateLimited, DEFAULT))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\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) 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/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRefreshToken   string `json:\"refresh_token\" required:\"true\"`\n\tDeviceID       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/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/Xhofe/rateg\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tlimitList func(ctx context.Context, data base.Json) (*Files, error)\n\tlimitLink func(ctx context.Context, file model.Obj) (*model.Link, error)\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\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(\"/adrive/v1.0/user/getDriveInfo\", http.MethodPost, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.DriveId = utils.Json.Get(res, d.DriveType+\"_drive_id\").ToString()\n\td.limitList = rateg.LimitFnCtx(d.list, rateg.LimitFnOption{\n\t\tLimit:  4,\n\t\tBucket: 1,\n\t})\n\td.limitLink = rateg.LimitFnCtx(d.link, rateg.LimitFnOption{\n\t\tLimit:  1,\n\t\tBucket: 1,\n\t})\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.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\tSize:     0,\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\tif d.limitList == nil {\n\t\treturn nil, fmt.Errorf(\"driver not init\")\n\t}\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) (*model.Link, error) {\n\tres, err := d.request(\"/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) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.limitLink == nil {\n\t\treturn nil, fmt.Errorf(\"driver not init\")\n\t}\n\treturn d.limitLink(ctx, file)\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(\"/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(\"/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(\"/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(\"/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(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(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\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/meta.go",
    "content": "package aliyundrive_open\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOauthTokenURL      string `json:\"oauth_token_url\" default:\"https://api.alistgo.com/alist/ali_open/token\"`\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\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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\tstreamPkg \"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/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(count int, fileId, uploadId string) ([]PartInfo, error) {\n\tpartInfoList := makePartInfos(count)\n\tvar resp CreateResp\n\t_, err := d.request(\"/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, \"PUT\", 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(fileId, uploadId string) (model.Obj, error) {\n\t// 3. complete\n\tvar newFile File\n\t_, err := d.request(\"/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 err == io.ErrUnexpectedEOF {\n\t\treturn \"\", fmt.Errorf(\"can't read data, expected=%d, got=%d\", len(buf), n)\n\t}\n\tif err != nil {\n\t\treturn \"\", 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(\"/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.CacheFullInTempFileAndHash(stream, 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(\"/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\n\t\tpreTime := time.Now()\n\t\tvar offset, length int64 = 0, partSize\n\t\t//var length\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(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 := utils.NewMultiReadable(io.LimitReader(stream, partSize))\n\t\t\tif rapidUpload {\n\t\t\t\tsrd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\trd = utils.NewMultiReadable(srd)\n\t\t\t}\n\t\t\terr = retry.Do(func() error {\n\t\t\t\t_ = rd.Reset()\n\t\t\t\trateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)\n\t\t\t\treturn d.uploadPart(ctx, rateLimitedRd, createResp.PartInfoList[i])\n\t\t\t},\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\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(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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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() (string, string, error) {\n\turl := API_URL + \"/oauth/access_token\"\n\tif d.OauthTokenURL != \"\" && d.ClientID == \"\" {\n\t\turl = d.OauthTokenURL\n\t}\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() error {\n\tif d.ref != nil {\n\t\treturn d.ref.refreshToken()\n\t}\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(\"[ali_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(\"[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(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {\n\tb, err, _ := d.requestReturnErrResp(uri, method, callback, retry...)\n\treturn b, err\n}\n\nfunc (d *AliyundriveOpen) requestReturnErrResp(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\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()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err, nil\n\t\t\t}\n\t\t\treturn d.requestReturnErrResp(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(\"/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.limitList(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\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Xhofe/rateg\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/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\tlimitList func(ctx context.Context, dir model.Obj) ([]model.Obj, error)\n\tlimitLink func(ctx context.Context, file model.Obj) (*model.Link, error)\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\terr := d.refreshToken()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = d.getShareToken()\n\tif err != 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()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t}\n\t})\n\td.limitList = rateg.LimitFnCtx(d.list, rateg.LimitFnOption{\n\t\tLimit:  4,\n\t\tBucket: 1,\n\t})\n\td.limitLink = rateg.LimitFnCtx(d.link, rateg.LimitFnOption{\n\t\tLimit:  1,\n\t\tBucket: 1,\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.DriveId = \"\"\n\treturn nil\n}\n\nfunc (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif d.limitList == nil {\n\t\treturn nil, fmt.Errorf(\"driver not init\")\n\t}\n\treturn d.limitList(ctx, dir)\n}\n\nfunc (d *AliyundriveShare) list(ctx context.Context, dir model.Obj) ([]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 *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.limitLink == nil {\n\t\treturn nil, fmt.Errorf(\"driver not init\")\n\t}\n\treturn d.limitLink(ctx, file)\n}\n\nfunc (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*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(\"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(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/meta.go",
    "content": "package aliyundrive_share\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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\"errors\"\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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() error {\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() error {\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(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\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()\n\t\t\t} else {\n\t\t\t\terr = d.getShareToken()\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(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(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\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()\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(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/alist-org/alist/v3/drivers/115\"\n\t_ \"github.com/alist-org/alist/v3/drivers/115_open\"\n\t_ \"github.com/alist-org/alist/v3/drivers/115_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/123\"\n\t_ \"github.com/alist-org/alist/v3/drivers/123_link\"\n\t_ \"github.com/alist-org/alist/v3/drivers/123_open\"\n\t_ \"github.com/alist-org/alist/v3/drivers/123_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/139\"\n\t_ \"github.com/alist-org/alist/v3/drivers/189\"\n\t_ \"github.com/alist-org/alist/v3/drivers/189pc\"\n\t_ \"github.com/alist-org/alist/v3/drivers/alias\"\n\t_ \"github.com/alist-org/alist/v3/drivers/alist_v2\"\n\t_ \"github.com/alist-org/alist/v3/drivers/alist_v3\"\n\t_ \"github.com/alist-org/alist/v3/drivers/aliyundrive\"\n\t_ \"github.com/alist-org/alist/v3/drivers/aliyundrive_open\"\n\t_ \"github.com/alist-org/alist/v3/drivers/aliyundrive_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/azure_blob\"\n\t_ \"github.com/alist-org/alist/v3/drivers/baidu_netdisk\"\n\t_ \"github.com/alist-org/alist/v3/drivers/baidu_photo\"\n\t_ \"github.com/alist-org/alist/v3/drivers/baidu_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/bitqiu\"\n\t_ \"github.com/alist-org/alist/v3/drivers/chaoxing\"\n\t_ \"github.com/alist-org/alist/v3/drivers/cloudreve\"\n\t_ \"github.com/alist-org/alist/v3/drivers/cloudreve_v4\"\n\t_ \"github.com/alist-org/alist/v3/drivers/crypt\"\n\t_ \"github.com/alist-org/alist/v3/drivers/doubao\"\n\t_ \"github.com/alist-org/alist/v3/drivers/doubao_new\"\n\t_ \"github.com/alist-org/alist/v3/drivers/doubao_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/dropbox\"\n\t_ \"github.com/alist-org/alist/v3/drivers/febbox\"\n\t_ \"github.com/alist-org/alist/v3/drivers/ftp\"\n\t_ \"github.com/alist-org/alist/v3/drivers/ftps\"\n\t_ \"github.com/alist-org/alist/v3/drivers/gitee\"\n\t_ \"github.com/alist-org/alist/v3/drivers/github\"\n\t_ \"github.com/alist-org/alist/v3/drivers/github_releases\"\n\t_ \"github.com/alist-org/alist/v3/drivers/gofile\"\n\t_ \"github.com/alist-org/alist/v3/drivers/google_drive\"\n\t_ \"github.com/alist-org/alist/v3/drivers/google_photo\"\n\t_ \"github.com/alist-org/alist/v3/drivers/halalcloud\"\n\t_ \"github.com/alist-org/alist/v3/drivers/ilanzou\"\n\t_ \"github.com/alist-org/alist/v3/drivers/ipfs_api\"\n\t_ \"github.com/alist-org/alist/v3/drivers/kodbox\"\n\t_ \"github.com/alist-org/alist/v3/drivers/lanzou\"\n\t_ \"github.com/alist-org/alist/v3/drivers/lenovonas_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/local\"\n\t_ \"github.com/alist-org/alist/v3/drivers/mediafire\"\n\t_ \"github.com/alist-org/alist/v3/drivers/mediatrack\"\n\t_ \"github.com/alist-org/alist/v3/drivers/mega\"\n\t_ \"github.com/alist-org/alist/v3/drivers/misskey\"\n\t_ \"github.com/alist-org/alist/v3/drivers/mopan\"\n\t_ \"github.com/alist-org/alist/v3/drivers/netease_music\"\n\t_ \"github.com/alist-org/alist/v3/drivers/onedrive\"\n\t_ \"github.com/alist-org/alist/v3/drivers/onedrive_app\"\n\t_ \"github.com/alist-org/alist/v3/drivers/onedrive_sharelink\"\n\t_ \"github.com/alist-org/alist/v3/drivers/pcloud\"\n\t_ \"github.com/alist-org/alist/v3/drivers/pikpak\"\n\t_ \"github.com/alist-org/alist/v3/drivers/pikpak_share\"\n\t_ \"github.com/alist-org/alist/v3/drivers/proton_drive\"\n\t_ \"github.com/alist-org/alist/v3/drivers/quark_uc\"\n\t_ \"github.com/alist-org/alist/v3/drivers/quark_uc_tv\"\n\t_ \"github.com/alist-org/alist/v3/drivers/quqi\"\n\t_ \"github.com/alist-org/alist/v3/drivers/s3\"\n\t_ \"github.com/alist-org/alist/v3/drivers/seafile\"\n\t_ \"github.com/alist-org/alist/v3/drivers/sftp\"\n\t_ \"github.com/alist-org/alist/v3/drivers/smb\"\n\t_ \"github.com/alist-org/alist/v3/drivers/streamtape\"\n\t_ \"github.com/alist-org/alist/v3/drivers/strm\"\n\t_ \"github.com/alist-org/alist/v3/drivers/teambition\"\n\t_ \"github.com/alist-org/alist/v3/drivers/terabox\"\n\t_ \"github.com/alist-org/alist/v3/drivers/thunder\"\n\t_ \"github.com/alist-org/alist/v3/drivers/thunder_browser\"\n\t_ \"github.com/alist-org/alist/v3/drivers/thunderx\"\n\t_ \"github.com/alist-org/alist/v3/drivers/trainbit\"\n\t_ \"github.com/alist-org/alist/v3/drivers/url_tree\"\n\t_ \"github.com/alist-org/alist/v3/drivers/uss\"\n\t_ \"github.com/alist-org/alist/v3/drivers/virtual\"\n\t_ \"github.com/alist-org/alist/v3/drivers/vtencent\"\n\t_ \"github.com/alist-org/alist/v3/drivers/webdav\"\n\t_ \"github.com/alist-org/alist/v3/drivers/weiyun\"\n\t_ \"github.com/alist-org/alist/v3/drivers/wopan\"\n\t_ \"github.com/alist-org/alist/v3/drivers/wukong\"\n\t_ \"github.com/alist-org/alist/v3/drivers/yandex_disk\"\n)\n\n// All do nothing,just for import\n// same as _ import\nfunc All() {\n\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\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\tconfig          driver.Config\n}\n\n// Config returns the driver configuration.\nfunc (d *AzureBlob) Config() driver.Config {\n\treturn d.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\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\tModified: *blobPrefix.Properties.LastModified,\n\t\t\t\tCtime:    *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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\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\n// implement GetRootId interface\nfunc (r Addition) GetRootId() string {\n\treturn r.ContainerName\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\t\tconfig: config,\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/azure_blob/types.go",
    "content": "package azure_blob\n\nimport \"github.com/alist-org/alist/v3/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\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/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\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\tupClient            *resty.Client // 上传文件使用的http客户端\n\tuploadUrlG          singleflight.Group[string]\n\tuploadUrlMu         sync.RWMutex\n\tuploadUrl           string    // 上传域名\n\tuploadUrlUpdateTime time.Time // 上传域名上次更新时间\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.upClient = base.NewRestyClient().\n\t\tSetTimeout(UPLOAD_TIMEOUT).\n\t\tSetRetryCount(UPLOAD_RETRY_COUNT).\n\t\tSetRetryWaitTime(UPLOAD_RETRY_WAIT_TIME).\n\t\tSetRetryMaxWaitTime(UPLOAD_RETRY_MAX_WAIT_TIME)\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\tif d.DownloadAPI == \"crack\" {\n\t\treturn d.linkCrack(file, args)\n\t} else if d.DownloadAPI == \"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 _, 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\tstreamSize := stream.GetSize()\n\tsliceSize := d.getSliceSize(streamSize)\n\tcount := int(streamSize / sliceSize)\n\tlastBlockSize := streamSize % sliceSize\n\tif lastBlockSize > 0 {\n\t\tcount++\n\t} else {\n\t\tlastBlockSize = sliceSize\n\t}\n\n\t//cal md5 for first 256k data\n\tconst SliceSize int64 = 256 * utils.KB\n\t// cal md5\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\treturn fileToObj(precreateResp.File), nil\n\t\t}\n\t}\n\n\t// step.2 上传分片\nuploadLoop:\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\t// 获取上传域名\n\t\tuploadUrl := d.getUploadUrl(path, precreateResp.Uploadid)\n\t\t// 并发上传\n\t\tthreadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,\n\t\t\tretry.Attempts(1),\n\t\t\tretry.Delay(time.Second),\n\t\t\tretry.DelayType(retry.BackOffDelay))\n\n\t\tcacheReaderAt, okReaderAt := cache.(io.ReaderAt)\n\t\tif !okReaderAt {\n\t\t\treturn nil, fmt.Errorf(\"cache object must implement io.ReaderAt interface for upload operations\")\n\t\t}\n\n\t\ttotalParts := len(precreateResp.BlockList)\n\t\tfor i, partseq := range precreateResp.BlockList {\n\t\t\tif utils.IsCanceled(upCtx) || partseq < 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\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(cacheReaderAt, offset, size)\n\t\t\t\terr := d.uploadSlice(ctx, uploadUrl, params, stream.GetName(), driver.NewLimitedUploadStream(ctx, 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\t// 当前goroutine还没退出，+1才是真正成功的数量\n\t\t\t\tsuccess := threadG.Success() + 1\n\t\t\t\tprogress := float64(success) * 100 / float64(totalParts)\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\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\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.Reader) error {\n\tres, err := d.upClient.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(params).\n\t\tSetFileReader(\"file\", fileName, file).\n\t\tPost(uploadUrl + \"/rest/2.0/pcs/superfile2\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugln(res.RawResponse.Status + res.String())\n\terrCode := utils.Json.Get(res.Body(), \"error_code\").ToInt()\n\terrNo := utils.Json.Get(res.Body(), \"errno\").ToInt()\n\trespStr := res.String()\n\tlower := strings.ToLower(respStr)\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\tif errCode != 0 || errNo != 0 {\n\t\treturn errs.NewErr(errs.StreamIncomplete, \"error uploading to baidu, response=%s\", res.String())\n\t}\n\treturn 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\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\tClientID              string `json:\"client_id\" required:\"true\" default:\"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf\"`\n\tClientSecret          string `json:\"client_secret\" required:\"true\" default:\"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE\"`\n\tCustomCrackUA         string `json:\"custom_crack_ua\" required:\"true\" default:\"netdisk\"`\n\tAccessToken           string\n\tUploadThread          string `json:\"upload_thread\" default:\"3\" help:\"1<=thread<=32\"`\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\tUPLOAD_TIMEOUT             = time.Minute * 30          // 上传请求超时时间\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}\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\n\t\t\t// 直接获取的MD5是错误的\n\t\t\tHashInfo: utils.NewHashInfo(utils.MD5, DecryptMd5(f.Md5)),\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\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"
  },
  {
    "path": "drivers/baidu_netdisk/util.go",
    "content": "package baidu_netdisk\n\nimport (\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\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.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 31023 == errno && 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 := 200\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\tstart += 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\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\":       fmt.Sprintf(\"%s\", 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\n// getUploadUrl 从开放平台获取上传域名/地址，并发请求会被合并，结果会被缓存1h。\n// 如果获取失败，则返回 Upload API设置项。\nfunc (d *BaiduNetdisk) getUploadUrl(path, uploadId string) string {\n\tif !d.UseDynamicUploadAPI {\n\t\treturn d.UploadAPI\n\t}\n\tgetCachedUrlFunc := func() string {\n\t\td.uploadUrlMu.RLock()\n\t\tdefer d.uploadUrlMu.RUnlock()\n\t\tif d.uploadUrl != \"\" && time.Since(d.uploadUrlUpdateTime) < UPLOAD_URL_EXPIRE_TIME {\n\t\t\treturn d.uploadUrl\n\t\t}\n\t\treturn \"\"\n\t}\n\t// 检查地址缓存\n\tif uploadUrl := getCachedUrlFunc(); uploadUrl != \"\" {\n\t\treturn uploadUrl\n\t}\n\n\tuploadUrlGetFunc := func() (string, error) {\n\t\t// 双重检查缓存\n\t\tif uploadUrl := getCachedUrlFunc(); uploadUrl != \"\" {\n\t\t\treturn uploadUrl, nil\n\t\t}\n\n\t\tuploadUrl, err := d.requestForUploadUrl(path, uploadId)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\td.uploadUrlMu.Lock()\n\t\tdefer d.uploadUrlMu.Unlock()\n\t\td.uploadUrl = uploadUrl\n\t\td.uploadUrlUpdateTime = time.Now()\n\t\treturn uploadUrl, nil\n\t}\n\n\tuploadUrl, err, _ := d.uploadUrlG.Do(\"\", uploadUrlGetFunc)\n\tif err != nil {\n\t\tfallback := d.UploadAPI\n\t\tlog.Warnf(\"[baidu_netdisk] get upload URL failed (%v), will use fallback URL: %s\", err, fallback)\n\t\treturn fallback\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\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\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/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 := int(streamSize / DEFAULT)\n\tlastBlockSize := streamSize % DEFAULT\n\tif lastBlockSize > 0 {\n\t\tcount++\n\t} else {\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\t\tsem := semaphore.NewWeighted(3)\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\tif err = sem.Acquire(ctx, 1); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer sem.Release(1)\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()) * 100 / float64(len(precreateResp.BlockList)))\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\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/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\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/alist-org/alist/v3/pkg/utils\"\n\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/baidu_share/driver.go",
    "content": "package baidu_share\n\nimport (\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\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype BaiduShare struct {\n\tmodel.Storage\n\tAddition\n\tclient *resty.Client\n\tinfo   struct {\n\t\tRoot    string\n\t\tSeckey  string\n\t\tShareid string\n\t\tUk      string\n\t}\n}\n\nfunc (d *BaiduShare) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *BaiduShare) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *BaiduShare) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\td.client = resty.New().\n\t\tSetBaseURL(\"https://pan.baidu.com\").\n\t\tSetHeader(\"User-Agent\", \"netdisk\").\n\t\tSetCookie(&http.Cookie{Name: \"BDUSS\", Value: d.BDUSS}).\n\t\tSetCookie(&http.Cookie{Name: \"ndut_fmt\"})\n\trespJson := struct {\n\t\tErrno int64 `json:\"errno\"`\n\t\tData  struct {\n\t\t\tList [1]struct {\n\t\t\t\tPath string `json:\"path\"`\n\t\t\t} `json:\"list\"`\n\t\t\tUk      json.Number `json:\"uk\"`\n\t\t\tShareid json.Number `json:\"shareid\"`\n\t\t\tSeckey  string      `json:\"seckey\"`\n\t\t} `json:\"data\"`\n\t}{}\n\tresp, err := d.client.R().\n\t\tSetBody(url.Values{\n\t\t\t\"pwd\":      {d.Pwd},\n\t\t\t\"root\":     {\"1\"},\n\t\t\t\"shorturl\": {d.Surl},\n\t\t}.Encode()).\n\t\tSetResult(&respJson).\n\t\tPost(\"share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1\")\n\tif err == nil {\n\t\tif resp.IsSuccess() && respJson.Errno == 0 {\n\t\t\td.info.Root = path.Dir(respJson.Data.List[0].Path)\n\t\t\td.info.Seckey = respJson.Data.Seckey\n\t\t\td.info.Shareid = respJson.Data.Shareid.String()\n\t\t\td.info.Uk = respJson.Data.Uk.String()\n\t\t} else {\n\t\t\terr = fmt.Errorf(\" %s; %s; \", resp.Status(), resp.Body())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *BaiduShare) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *BaiduShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\t// TODO return the files list, required\n\treqDir := dir.GetPath()\n\tisRoot := \"0\"\n\tif reqDir == d.RootFolderPath {\n\t\treqDir = path.Join(d.info.Root, reqDir)\n\t}\n\tif reqDir == d.info.Root {\n\t\tisRoot = \"1\"\n\t}\n\tobjs := []model.Obj{}\n\tvar err error\n\tvar page uint64 = 1\n\tmore := true\n\tfor more && err == nil {\n\t\trespJson := struct {\n\t\t\tErrno int64 `json:\"errno\"`\n\t\t\tData  struct {\n\t\t\t\tMore bool `json:\"has_more\"`\n\t\t\t\tList []struct {\n\t\t\t\t\tFsid  json.Number `json:\"fs_id\"`\n\t\t\t\t\tIsdir json.Number `json:\"isdir\"`\n\t\t\t\t\tPath  string      `json:\"path\"`\n\t\t\t\t\tName  string      `json:\"server_filename\"`\n\t\t\t\t\tMtime json.Number `json:\"server_mtime\"`\n\t\t\t\t\tSize  json.Number `json:\"size\"`\n\t\t\t\t} `json:\"list\"`\n\t\t\t} `json:\"data\"`\n\t\t}{}\n\t\tresp, e := d.client.R().\n\t\t\tSetBody(url.Values{\n\t\t\t\t\"dir\":      {reqDir},\n\t\t\t\t\"num\":      {\"1000\"},\n\t\t\t\t\"order\":    {\"time\"},\n\t\t\t\t\"page\":     {fmt.Sprint(page)},\n\t\t\t\t\"pwd\":      {d.Pwd},\n\t\t\t\t\"root\":     {isRoot},\n\t\t\t\t\"shorturl\": {d.Surl},\n\t\t\t}.Encode()).\n\t\t\tSetResult(&respJson).\n\t\t\tPost(\"share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1\")\n\t\terr = e\n\t\tif err == nil {\n\t\t\tif resp.IsSuccess() && respJson.Errno == 0 {\n\t\t\t\tpage++\n\t\t\t\tmore = respJson.Data.More\n\t\t\t\tfor _, v := range respJson.Data.List {\n\t\t\t\t\tsize, _ := v.Size.Int64()\n\t\t\t\t\tmtime, _ := v.Mtime.Int64()\n\t\t\t\t\tobjs = append(objs, &model.Object{\n\t\t\t\t\t\tID:       v.Fsid.String(),\n\t\t\t\t\t\tPath:     v.Path,\n\t\t\t\t\t\tName:     v.Name,\n\t\t\t\t\t\tSize:     size,\n\t\t\t\t\t\tModified: time.Unix(mtime, 0),\n\t\t\t\t\t\tIsFolder: v.Isdir.String() == \"1\",\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\" %s; %s; \", resp.Status(), resp.Body())\n\t\t\t}\n\t\t}\n\t}\n\treturn objs, err\n}\n\nfunc (d *BaiduShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\t// TODO return link of file, required\n\tlink := model.Link{Header: d.client.Header}\n\tsign := \"\"\n\tstamp := \"\"\n\tsignJson := struct {\n\t\tErrno int64 `json:\"errno\"`\n\t\tData  struct {\n\t\t\tStamp json.Number `json:\"timestamp\"`\n\t\t\tSign  string      `json:\"sign\"`\n\t\t} `json:\"data\"`\n\t}{}\n\tresp, err := d.client.R().\n\t\tSetQueryParam(\"surl\", d.Surl).\n\t\tSetResult(&signJson).\n\t\tGet(\"share/tplconfig?fields=sign,timestamp&channel=chunlei&web=1&app_id=250528&clienttype=0\")\n\tif err == nil {\n\t\tif resp.IsSuccess() && signJson.Errno == 0 {\n\t\t\tstamp = signJson.Data.Stamp.String()\n\t\t\tsign = signJson.Data.Sign\n\t\t} else {\n\t\t\terr = fmt.Errorf(\" %s; %s; \", resp.Status(), resp.Body())\n\t\t}\n\t}\n\tif err == nil {\n\t\trespJson := struct {\n\t\t\tErrno int64 `json:\"errno\"`\n\t\t\tList  [1]struct {\n\t\t\t\tDlink string `json:\"dlink\"`\n\t\t\t} `json:\"list\"`\n\t\t}{}\n\t\tresp, err = d.client.R().\n\t\t\tSetQueryParam(\"sign\", sign).\n\t\t\tSetQueryParam(\"timestamp\", stamp).\n\t\t\tSetBody(url.Values{\n\t\t\t\t\"encrypt\":   {\"0\"},\n\t\t\t\t\"extra\":     {fmt.Sprintf(`{\"sekey\":\"%s\"}`, d.info.Seckey)},\n\t\t\t\t\"fid_list\":  {fmt.Sprintf(\"[%s]\", file.GetID())},\n\t\t\t\t\"primaryid\": {d.info.Shareid},\n\t\t\t\t\"product\":   {\"share\"},\n\t\t\t\t\"type\":      {\"nolimit\"},\n\t\t\t\t\"uk\":        {d.info.Uk},\n\t\t\t}.Encode()).\n\t\t\tSetResult(&respJson).\n\t\t\tPost(\"api/sharedownload?app_id=250528&channel=chunlei&clienttype=12&web=1\")\n\t\tif err == nil {\n\t\t\tif resp.IsSuccess() && respJson.Errno == 0 && respJson.List[0].Dlink != \"\" {\n\t\t\t\tlink.URL = respJson.List[0].Dlink\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\" %s; %s; \", resp.Status(), resp.Body())\n\t\t\t}\n\t\t}\n\t\tif err == nil {\n\t\t\tresp, err = d.client.R().\n\t\t\t\tSetDoNotParseResponse(true).\n\t\t\t\tGet(link.URL)\n\t\t\tif err == nil {\n\t\t\t\tdefer resp.RawBody().Close()\n\t\t\t\tif resp.IsError() {\n\t\t\t\t\tbyt, _ := io.ReadAll(resp.RawBody())\n\t\t\t\t\terr = fmt.Errorf(\" %s; %s; \", resp.Status(), byt)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &link, err\n}\n\nfunc (d *BaiduShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\t// TODO create folder, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO move obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\t// TODO rename obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO copy obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduShare) Remove(ctx context.Context, obj model.Obj) error {\n\t// TODO remove obj, optional\n\treturn errs.NotSupport\n}\n\nfunc (d *BaiduShare) 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 *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*BaiduShare)(nil)\n"
  },
  {
    "path": "drivers/baidu_share/meta.go",
    "content": "package baidu_share\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\t// Field string `json:\"field\" type:\"select\" required:\"true\" options:\"a,b,c\" default:\"a\"`\n\tSurl  string `json:\"surl\"`\n\tPwd   string `json:\"pwd\"`\n\tBDUSS string `json:\"BDUSS\"`\n}\n\nvar config = driver.Config{\n\tName:              \"BaiduShare\",\n\tLocalSort:         true,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          true,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &BaiduShare{}\n\t})\n}\n"
  },
  {
    "path": "drivers/baidu_share/types.go",
    "content": "package baidu_share\n"
  },
  {
    "path": "drivers/baidu_share/util.go",
    "content": "package baidu_share\n\n// do others that not defined in Driver interface\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/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)\nvar UserAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36\"\nvar DefaultTimeout = time.Second * 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\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\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/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\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/bitqiu/driver.go",
    "content": "package bitqiu\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http/cookiejar\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\tstreamPkg \"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n)\n\nconst (\n\tbaseURL             = \"https://pan.bitqiu.com\"\n\tloginURL            = baseURL + \"/loginServer/login\"\n\tuserInfoURL         = baseURL + \"/user/getInfo\"\n\tlistURL             = baseURL + \"/apiToken/cfi/fs/resources/pages\"\n\tuploadInitializeURL = baseURL + \"/apiToken/cfi/fs/upload/v2/initialize\"\n\tuploadCompleteURL   = baseURL + \"/apiToken/cfi/fs/upload/v2/complete\"\n\tdownloadURL         = baseURL + \"/download/getUrl\"\n\tcreateDirURL        = baseURL + \"/resource/create\"\n\tmoveResourceURL     = baseURL + \"/resource/remove\"\n\trenameResourceURL   = baseURL + \"/resource/rename\"\n\tcopyResourceURL     = baseURL + \"/apiToken/cfi/fs/async/copy\"\n\tcopyManagerURL      = baseURL + \"/apiToken/cfi/fs/async/manager\"\n\tdeleteResourceURL   = baseURL + \"/resource/delete\"\n\n\tsuccessCode       = \"10200\"\n\tuploadSuccessCode = \"30010\"\n\tcopySubmittedCode = \"10300\"\n\torgChannel        = \"default|default|default\"\n)\n\nconst (\n\tcopyPollInterval    = time.Second\n\tcopyPollMaxAttempts = 60\n\tchunkSize           = int64(1 << 20)\n)\n\nconst defaultUserAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36\"\n\ntype BitQiu struct {\n\tmodel.Storage\n\tAddition\n\n\tclient *resty.Client\n\tuserID string\n}\n\nfunc (d *BitQiu) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *BitQiu) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *BitQiu) Init(ctx context.Context) error {\n\tif d.Addition.UserPlatform == \"\" {\n\t\td.Addition.UserPlatform = uuid.NewString()\n\t\top.MustSaveDriverStorage(d)\n\t}\n\n\tif d.client == nil {\n\t\tjar, err := cookiejar.New(nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.client = base.NewRestyClient()\n\t\td.client.SetBaseURL(baseURL)\n\t\td.client.SetCookieJar(jar)\n\t}\n\td.client.SetHeader(\"user-agent\", d.userAgent())\n\n\treturn d.login(ctx)\n}\n\nfunc (d *BitQiu) Drop(ctx context.Context) error {\n\td.client = nil\n\td.userID = \"\"\n\treturn nil\n}\n\nfunc (d *BitQiu) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tparentID := d.resolveParentID(dir)\n\tdirPath := \"\"\n\tif dir != nil {\n\t\tdirPath = dir.GetPath()\n\t}\n\tpageSize := d.pageSize()\n\torderType := d.orderType()\n\tdesc := d.orderDesc()\n\n\tvar results []model.Obj\n\tpage := 1\n\tfor {\n\t\tform := map[string]string{\n\t\t\t\"parentId\":    parentID,\n\t\t\t\"limit\":       strconv.Itoa(pageSize),\n\t\t\t\"orderType\":   orderType,\n\t\t\t\"desc\":        desc,\n\t\t\t\"model\":       \"1\",\n\t\t\t\"userId\":      d.userID,\n\t\t\t\"currentPage\": strconv.Itoa(page),\n\t\t\t\"page\":        strconv.Itoa(page),\n\t\t\t\"org_channel\": orgChannel,\n\t\t}\n\t\tvar resp Response[ResourcePage]\n\t\tif err := d.postForm(ctx, listURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resp.Code != successCode {\n\t\t\tif resp.Code == \"10401\" || resp.Code == \"10404\" {\n\t\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"list failed: %s\", resp.Message)\n\t\t}\n\n\t\tobjs, err := utils.SliceConvert(resp.Data.Data, func(item Resource) (model.Obj, error) {\n\t\t\treturn item.toObject(parentID, dirPath)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresults = append(results, objs...)\n\n\t\tif !resp.Data.HasNext || len(resp.Data.Data) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\n\n\treturn results, nil\n}\n\nfunc (d *BitQiu) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.IsDir() {\n\t\treturn nil, errs.NotFile\n\t}\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tform := map[string]string{\n\t\t\"fileIds\":     file.GetID(),\n\t\t\"org_channel\": orgChannel,\n\t}\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[DownloadData]\n\t\tif err := d.postForm(ctx, downloadURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\tif resp.Data.URL == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"empty download url returned\")\n\t\t\t}\n\t\t\treturn &model.Link{URL: resp.Data.URL}, nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"get link failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"get link failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tparentID := d.resolveParentID(parentDir)\n\tparentPath := \"\"\n\tif parentDir != nil {\n\t\tparentPath = parentDir.GetPath()\n\t}\n\tform := map[string]string{\n\t\t\"parentId\":    parentID,\n\t\t\"name\":        dirName,\n\t\t\"org_channel\": orgChannel,\n\t}\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[CreateDirData]\n\t\tif err := d.postForm(ctx, createDirURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\tnewParentID := parentID\n\t\t\tif resp.Data.ParentID != \"\" {\n\t\t\t\tnewParentID = resp.Data.ParentID\n\t\t\t}\n\t\t\tname := resp.Data.Name\n\t\t\tif name == \"\" {\n\t\t\t\tname = dirName\n\t\t\t}\n\t\t\tresource := Resource{\n\t\t\t\tResourceID:   resp.Data.DirID,\n\t\t\t\tResourceType: 1,\n\t\t\t\tName:         name,\n\t\t\t\tParentID:     newParentID,\n\t\t\t}\n\t\t\tobj, err := resource.toObject(newParentID, parentPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif o, ok := obj.(*Object); ok {\n\t\t\t\to.ParentID = newParentID\n\t\t\t}\n\t\t\treturn obj, nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"create folder failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"create folder failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\ttargetParentID := d.resolveParentID(dstDir)\n\tform := map[string]string{\n\t\t\"dirIds\":      \"\",\n\t\t\"fileIds\":     \"\",\n\t\t\"parentId\":    targetParentID,\n\t\t\"org_channel\": orgChannel,\n\t}\n\tif srcObj.IsDir() {\n\t\tform[\"dirIds\"] = srcObj.GetID()\n\t} else {\n\t\tform[\"fileIds\"] = srcObj.GetID()\n\t}\n\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[any]\n\t\tif err := d.postForm(ctx, moveResourceURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\tdstPath := \"\"\n\t\t\tif dstDir != nil {\n\t\t\t\tdstPath = dstDir.GetPath()\n\t\t\t}\n\t\t\tif setter, ok := srcObj.(model.SetPath); ok {\n\t\t\t\tsetter.SetPath(path.Join(dstPath, srcObj.GetName()))\n\t\t\t}\n\t\t\tif o, ok := srcObj.(*Object); ok {\n\t\t\t\to.ParentID = targetParentID\n\t\t\t}\n\t\t\treturn srcObj, nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"move failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"move failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tform := map[string]string{\n\t\t\"resourceId\":  srcObj.GetID(),\n\t\t\"name\":        newName,\n\t\t\"type\":        \"0\",\n\t\t\"org_channel\": orgChannel,\n\t}\n\tif srcObj.IsDir() {\n\t\tform[\"type\"] = \"1\"\n\t}\n\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[any]\n\t\tif err := d.postForm(ctx, renameResourceURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\treturn updateObjectName(srcObj, newName), nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"rename failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"rename failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\ttargetParentID := d.resolveParentID(dstDir)\n\tform := map[string]string{\n\t\t\"dirIds\":      \"\",\n\t\t\"fileIds\":     \"\",\n\t\t\"parentId\":    targetParentID,\n\t\t\"org_channel\": orgChannel,\n\t}\n\tif srcObj.IsDir() {\n\t\tform[\"dirIds\"] = srcObj.GetID()\n\t} else {\n\t\tform[\"fileIds\"] = srcObj.GetID()\n\t}\n\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[any]\n\t\tif err := d.postForm(ctx, copyResourceURL, form, &resp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode, copySubmittedCode:\n\t\t\treturn d.waitForCopiedObject(ctx, srcObj, dstDir)\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"copy failed: %s\", resp.Message)\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"copy failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) Remove(ctx context.Context, obj model.Obj) error {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tform := map[string]string{\n\t\t\"dirIds\":      \"\",\n\t\t\"fileIds\":     \"\",\n\t\t\"org_channel\": orgChannel,\n\t}\n\tif obj.IsDir() {\n\t\tform[\"dirIds\"] = obj.GetID()\n\t} else {\n\t\tform[\"fileIds\"] = obj.GetID()\n\t}\n\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[any]\n\t\tif err := d.postForm(ctx, deleteResourceURL, form, &resp); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\treturn nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"remove failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"remove failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif d.userID == \"\" {\n\t\tif err := d.login(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tup(0)\n\ttmpFile, md5sum, err := streamPkg.CacheFullInTempFileAndHash(file, utils.MD5)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tmpFile.Close()\n\n\tparentID := d.resolveParentID(dstDir)\n\tparentPath := \"\"\n\tif dstDir != nil {\n\t\tparentPath = dstDir.GetPath()\n\t}\n\tform := map[string]string{\n\t\t\"parentId\":    parentID,\n\t\t\"name\":        file.GetName(),\n\t\t\"size\":        strconv.FormatInt(file.GetSize(), 10),\n\t\t\"hash\":        md5sum,\n\t\t\"sampleMd5\":   md5sum,\n\t\t\"org_channel\": orgChannel,\n\t}\n\tvar resp Response[json.RawMessage]\n\tif err = d.postForm(ctx, uploadInitializeURL, form, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Code != uploadSuccessCode {\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\tvar initData UploadInitData\n\t\t\tif err := json.Unmarshal(resp.Data, &initData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"parse upload init response failed: %w\", err)\n\t\t\t}\n\t\t\tserverCode, err := d.uploadFileInChunks(ctx, tmpFile, file.GetSize(), md5sum, initData, up)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tobj, err := d.completeChunkUpload(ctx, initData, parentID, parentPath, file.GetName(), file.GetSize(), md5sum, serverCode)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tup(100)\n\t\t\treturn obj, nil\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"upload failed: %s\", resp.Message)\n\t\t}\n\t}\n\n\tvar resource Resource\n\tif err := json.Unmarshal(resp.Data, &resource); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse upload response failed: %w\", err)\n\t}\n\tobj, err := resource.toObject(parentID, parentPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tup(100)\n\treturn obj, nil\n}\n\nfunc (d *BitQiu) uploadFileInChunks(ctx context.Context, tmpFile model.File, size int64, md5sum string, initData UploadInitData, up driver.UpdateProgress) (string, error) {\n\tif d.client == nil {\n\t\treturn \"\", fmt.Errorf(\"client not initialized\")\n\t}\n\tif size <= 0 {\n\t\treturn \"\", fmt.Errorf(\"invalid file size\")\n\t}\n\tbuf := make([]byte, chunkSize)\n\toffset := int64(0)\n\tvar finishedFlag string\n\n\tfor offset < size {\n\t\tchunkLen := chunkSize\n\t\tremaining := size - offset\n\t\tif remaining < chunkLen {\n\t\t\tchunkLen = remaining\n\t\t}\n\n\t\treader := io.NewSectionReader(tmpFile, offset, chunkLen)\n\t\tchunkBuf := buf[:chunkLen]\n\t\tif _, err := io.ReadFull(reader, chunkBuf); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"read chunk failed: %w\", err)\n\t\t}\n\n\t\theaders := map[string]string{\n\t\t\t\"accept\":       \"*/*\",\n\t\t\t\"content-type\": \"application/octet-stream\",\n\t\t\t\"appid\":        initData.AppID,\n\t\t\t\"token\":        initData.Token,\n\t\t\t\"userid\":       strconv.FormatInt(initData.UserID, 10),\n\t\t\t\"serialnumber\": initData.SerialNumber,\n\t\t\t\"hash\":         md5sum,\n\t\t\t\"len\":          strconv.FormatInt(chunkLen, 10),\n\t\t\t\"offset\":       strconv.FormatInt(offset, 10),\n\t\t\t\"user-agent\":   d.userAgent(),\n\t\t}\n\n\t\tvar chunkResp ChunkUploadResponse\n\t\treq := d.client.R().\n\t\t\tSetContext(ctx).\n\t\t\tSetHeaders(headers).\n\t\t\tSetBody(chunkBuf).\n\t\t\tSetResult(&chunkResp)\n\n\t\tif _, err := req.Post(initData.UploadURL); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif chunkResp.ErrCode != 0 {\n\t\t\treturn \"\", fmt.Errorf(\"chunk upload failed with code %d\", chunkResp.ErrCode)\n\t\t}\n\t\tfinishedFlag = chunkResp.FinishedFlag\n\t\toffset += chunkLen\n\t\tup(float64(offset) * 100 / float64(size))\n\t}\n\n\tif finishedFlag == \"\" {\n\t\treturn \"\", fmt.Errorf(\"upload finished without server code\")\n\t}\n\treturn finishedFlag, nil\n}\n\nfunc (d *BitQiu) completeChunkUpload(ctx context.Context, initData UploadInitData, parentID, parentPath, name string, size int64, md5sum, serverCode string) (model.Obj, error) {\n\tform := map[string]string{\n\t\t\"currentPage\": \"1\",\n\t\t\"limit\":       \"1\",\n\t\t\"userId\":      strconv.FormatInt(initData.UserID, 10),\n\t\t\"status\":      \"0\",\n\t\t\"parentId\":    parentID,\n\t\t\"name\":        name,\n\t\t\"fileUid\":     initData.FileUID,\n\t\t\"fileSid\":     initData.FileSID,\n\t\t\"size\":        strconv.FormatInt(size, 10),\n\t\t\"serverCode\":  serverCode,\n\t\t\"snapTime\":    \"\",\n\t\t\"hash\":        md5sum,\n\t\t\"sampleMd5\":   md5sum,\n\t\t\"org_channel\": orgChannel,\n\t}\n\n\tvar resp Response[Resource]\n\tif err := d.postForm(ctx, uploadCompleteURL, form, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Code != successCode {\n\t\treturn nil, fmt.Errorf(\"complete upload failed: %s\", resp.Message)\n\t}\n\n\treturn resp.Data.toObject(parentID, parentPath)\n}\n\nfunc (d *BitQiu) login(ctx context.Context) error {\n\tif d.client == nil {\n\t\treturn fmt.Errorf(\"client not initialized\")\n\t}\n\n\tform := map[string]string{\n\t\t\"passport\":    d.Username,\n\t\t\"password\":    utils.GetMD5EncodeStr(d.Password),\n\t\t\"remember\":    \"0\",\n\t\t\"captcha\":     \"\",\n\t\t\"org_channel\": orgChannel,\n\t}\n\tvar resp Response[LoginData]\n\tif err := d.postForm(ctx, loginURL, form, &resp); err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != successCode {\n\t\treturn fmt.Errorf(\"login failed: %s\", resp.Message)\n\t}\n\td.userID = strconv.FormatInt(resp.Data.UserID, 10)\n\treturn d.ensureRootFolderID(ctx)\n}\n\nfunc (d *BitQiu) ensureRootFolderID(ctx context.Context) error {\n\trootID := d.Addition.GetRootId()\n\tif rootID != \"\" && rootID != \"0\" {\n\t\treturn nil\n\t}\n\n\tform := map[string]string{\n\t\t\"org_channel\": orgChannel,\n\t}\n\tvar resp Response[UserInfoData]\n\tif err := d.postForm(ctx, userInfoURL, form, &resp); err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != successCode {\n\t\treturn fmt.Errorf(\"get user info failed: %s\", resp.Message)\n\t}\n\tif resp.Data.RootDirID == \"\" {\n\t\treturn fmt.Errorf(\"get user info failed: empty root dir id\")\n\t}\n\tif d.Addition.RootFolderID != resp.Data.RootDirID {\n\t\td.Addition.RootFolderID = resp.Data.RootDirID\n\t\top.MustSaveDriverStorage(d)\n\t}\n\treturn nil\n}\n\nfunc (d *BitQiu) postForm(ctx context.Context, url string, form map[string]string, result interface{}) error {\n\tif d.client == nil {\n\t\treturn fmt.Errorf(\"client not initialized\")\n\t}\n\treq := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetHeaders(d.commonHeaders()).\n\t\tSetFormData(form)\n\tif result != nil {\n\t\treq = req.SetResult(result)\n\t}\n\t_, err := req.Post(url)\n\treturn err\n}\n\nfunc (d *BitQiu) waitForCopiedObject(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\texpectedName := srcObj.GetName()\n\texpectedIsDir := srcObj.IsDir()\n\tvar lastListErr error\n\n\tfor attempt := 0; attempt < copyPollMaxAttempts; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tif err := waitWithContext(ctx, copyPollInterval); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tif err := d.checkCopyFailure(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tobj, err := d.findObjectInDir(ctx, dstDir, expectedName, expectedIsDir)\n\t\tif err != nil {\n\t\t\tlastListErr = err\n\t\t\tcontinue\n\t\t}\n\t\tif obj != nil {\n\t\t\treturn obj, nil\n\t\t}\n\t}\n\tif lastListErr != nil {\n\t\treturn nil, lastListErr\n\t}\n\treturn nil, fmt.Errorf(\"copy task timed out waiting for completion\")\n}\n\nfunc (d *BitQiu) checkCopyFailure(ctx context.Context) error {\n\tform := map[string]string{\n\t\t\"org_channel\": orgChannel,\n\t}\n\tfor attempt := 0; attempt < 2; attempt++ {\n\t\tvar resp Response[AsyncManagerData]\n\t\tif err := d.postForm(ctx, copyManagerURL, form, &resp); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch resp.Code {\n\t\tcase successCode:\n\t\t\tif len(resp.Data.FailTasks) > 0 {\n\t\t\t\treturn fmt.Errorf(\"copy failed: %s\", resp.Data.FailTasks[0].ErrorMessage())\n\t\t\t}\n\t\t\treturn nil\n\t\tcase \"10401\", \"10404\":\n\t\t\tif err := d.login(ctx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"query copy status failed: %s\", resp.Message)\n\t\t}\n\t}\n\treturn fmt.Errorf(\"query copy status failed: retry limit reached\")\n}\n\nfunc (d *BitQiu) findObjectInDir(ctx context.Context, dir model.Obj, name string, isDir bool) (model.Obj, error) {\n\tobjs, err := d.List(ctx, dir, model.ListArgs{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, obj := range objs {\n\t\tif obj.GetName() == name && obj.IsDir() == isDir {\n\t\t\treturn obj, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc waitWithContext(ctx context.Context, d time.Duration) error {\n\ttimer := time.NewTimer(d)\n\tdefer timer.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n\nfunc (d *BitQiu) commonHeaders() map[string]string {\n\theaders := map[string]string{\n\t\t\"accept\":                 \"application/json, text/plain, */*\",\n\t\t\"accept-language\":        \"en-US,en;q=0.9\",\n\t\t\"cache-control\":          \"no-cache\",\n\t\t\"pragma\":                 \"no-cache\",\n\t\t\"user-platform\":          d.Addition.UserPlatform,\n\t\t\"x-kl-saas-ajax-request\": \"Ajax_Request\",\n\t\t\"x-requested-with\":       \"XMLHttpRequest\",\n\t\t\"referer\":                baseURL + \"/\",\n\t\t\"origin\":                 baseURL,\n\t\t\"user-agent\":             d.userAgent(),\n\t}\n\treturn headers\n}\n\nfunc (d *BitQiu) userAgent() string {\n\tif ua := strings.TrimSpace(d.Addition.UserAgent); ua != \"\" {\n\t\treturn ua\n\t}\n\treturn defaultUserAgent\n}\n\nfunc (d *BitQiu) resolveParentID(dir model.Obj) string {\n\tif dir != nil && dir.GetID() != \"\" {\n\t\treturn dir.GetID()\n\t}\n\tif root := d.Addition.GetRootId(); root != \"\" {\n\t\treturn root\n\t}\n\treturn config.DefaultRoot\n}\n\nfunc (d *BitQiu) pageSize() int {\n\tif size, err := strconv.Atoi(d.Addition.PageSize); err == nil && size > 0 {\n\t\treturn size\n\t}\n\treturn 24\n}\n\nfunc (d *BitQiu) orderType() string {\n\tif d.Addition.OrderType != \"\" {\n\t\treturn d.Addition.OrderType\n\t}\n\treturn \"updateTime\"\n}\n\nfunc (d *BitQiu) orderDesc() string {\n\tif d.Addition.OrderDesc {\n\t\treturn \"1\"\n\t}\n\treturn \"0\"\n}\n\nvar _ driver.Driver = (*BitQiu)(nil)\nvar _ driver.PutResult = (*BitQiu)(nil)\n"
  },
  {
    "path": "drivers/bitqiu/meta.go",
    "content": "package bitqiu\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tUserPlatform string `json:\"user_platform\" help:\"Optional device identifier; auto-generated if empty.\"`\n\tOrderType    string `json:\"order_type\" type:\"select\" options:\"updateTime,createTime,name,size\" default:\"updateTime\"`\n\tOrderDesc    bool   `json:\"order_desc\"`\n\tPageSize     string `json:\"page_size\" default:\"24\" help:\"Number of entries to request per page.\"`\n\tUserAgent    string `json:\"user_agent\" default:\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36\"`\n}\n\nvar config = driver.Config{\n\tName:        \"BitQiu\",\n\tDefaultRoot: \"0\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &BitQiu{}\n\t})\n}\n"
  },
  {
    "path": "drivers/bitqiu/types.go",
    "content": "package bitqiu\n\nimport \"encoding/json\"\n\ntype Response[T any] struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    T      `json:\"data\"`\n}\n\ntype LoginData struct {\n\tUserID int64 `json:\"userId\"`\n}\n\ntype ResourcePage struct {\n\tCurrentPage    int        `json:\"currentPage\"`\n\tPageSize       int        `json:\"pageSize\"`\n\tTotalCount     int        `json:\"totalCount\"`\n\tTotalPageCount int        `json:\"totalPageCount\"`\n\tData           []Resource `json:\"data\"`\n\tHasNext        bool       `json:\"hasNext\"`\n}\n\ntype Resource struct {\n\tResourceID   string       `json:\"resourceId\"`\n\tResourceUID  string       `json:\"resourceUid\"`\n\tResourceType int          `json:\"resourceType\"`\n\tParentID     string       `json:\"parentId\"`\n\tName         string       `json:\"name\"`\n\tExtName      string       `json:\"extName\"`\n\tSize         *json.Number `json:\"size\"`\n\tCreateTime   *string      `json:\"createTime\"`\n\tUpdateTime   *string      `json:\"updateTime\"`\n\tFileMD5      string       `json:\"fileMd5\"`\n}\n\ntype DownloadData struct {\n\tURL  string `json:\"url\"`\n\tMD5  string `json:\"md5\"`\n\tSize int64  `json:\"size\"`\n}\n\ntype UserInfoData struct {\n\tRootDirID string `json:\"rootDirId\"`\n}\n\ntype CreateDirData struct {\n\tDirID    string `json:\"dirId\"`\n\tName     string `json:\"name\"`\n\tParentID string `json:\"parentId\"`\n}\n\ntype AsyncManagerData struct {\n\tWaitTasks    []AsyncTask `json:\"waitTaskList\"`\n\tRunningTasks []AsyncTask `json:\"runningTaskList\"`\n\tSuccessTasks []AsyncTask `json:\"successTaskList\"`\n\tFailTasks    []AsyncTask `json:\"failTaskList\"`\n\tTaskList     []AsyncTask `json:\"taskList\"`\n}\n\ntype AsyncTask struct {\n\tTaskID      string         `json:\"taskId\"`\n\tStatus      int            `json:\"status\"`\n\tErrorMsg    string         `json:\"errorMsg\"`\n\tMessage     string         `json:\"message\"`\n\tResult      *AsyncTaskInfo `json:\"result\"`\n\tTargetName  string         `json:\"targetName\"`\n\tTargetDirID string         `json:\"parentId\"`\n}\n\ntype AsyncTaskInfo struct {\n\tResource Resource `json:\"resource\"`\n\tDirID    string   `json:\"dirId\"`\n\tFileID   string   `json:\"fileId\"`\n\tName     string   `json:\"name\"`\n\tParentID string   `json:\"parentId\"`\n}\n\nfunc (t AsyncTask) ErrorMessage() string {\n\tif t.ErrorMsg != \"\" {\n\t\treturn t.ErrorMsg\n\t}\n\tif t.Message != \"\" {\n\t\treturn t.Message\n\t}\n\treturn \"unknown error\"\n}\n\ntype UploadInitData struct {\n\tName         string `json:\"name\"`\n\tSize         int64  `json:\"size\"`\n\tToken        string `json:\"token\"`\n\tFileUID      string `json:\"fileUid\"`\n\tFileSID      string `json:\"fileSid\"`\n\tParentID     string `json:\"parentId\"`\n\tUserID       int64  `json:\"userId\"`\n\tSerialNumber string `json:\"serialNumber\"`\n\tUploadURL    string `json:\"uploadUrl\"`\n\tAppID        string `json:\"appId\"`\n}\n\ntype ChunkUploadResponse struct {\n\tErrCode      int    `json:\"errCode\"`\n\tOffset       int64  `json:\"offset\"`\n\tFinished     int    `json:\"finished\"`\n\tFinishedFlag string `json:\"finishedFlag\"`\n}\n"
  },
  {
    "path": "drivers/bitqiu/util.go",
    "content": "package bitqiu\n\nimport (\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype Object struct {\n\tmodel.Object\n\tParentID string\n}\n\nfunc (r Resource) toObject(parentID, parentPath string) (model.Obj, error) {\n\tid := r.ResourceID\n\tif id == \"\" {\n\t\tid = r.ResourceUID\n\t}\n\tobj := &Object{\n\t\tObject: model.Object{\n\t\t\tID:       id,\n\t\t\tName:     r.Name,\n\t\t\tIsFolder: r.ResourceType == 1,\n\t\t},\n\t\tParentID: parentID,\n\t}\n\tif r.Size != nil {\n\t\tif size, err := (*r.Size).Int64(); err == nil {\n\t\t\tobj.Size = size\n\t\t}\n\t}\n\tif ct := parseBitQiuTime(r.CreateTime); !ct.IsZero() {\n\t\tobj.Ctime = ct\n\t}\n\tif mt := parseBitQiuTime(r.UpdateTime); !mt.IsZero() {\n\t\tobj.Modified = mt\n\t}\n\tif r.FileMD5 != \"\" {\n\t\tobj.HashInfo = utils.NewHashInfo(utils.MD5, strings.ToLower(r.FileMD5))\n\t}\n\tobj.SetPath(path.Join(parentPath, obj.Name))\n\treturn obj, nil\n}\n\nfunc parseBitQiuTime(value *string) time.Time {\n\tif value == nil {\n\t\treturn time.Time{}\n\t}\n\ttrimmed := strings.TrimSpace(*value)\n\tif trimmed == \"\" {\n\t\treturn time.Time{}\n\t}\n\tif ts, err := time.ParseInLocation(\"2006-01-02 15:04:05\", trimmed, time.Local); err == nil {\n\t\treturn ts\n\t}\n\treturn time.Time{}\n}\n\nfunc updateObjectName(obj model.Obj, newName string) model.Obj {\n\tnewPath := path.Join(parentPathOf(obj.GetPath()), newName)\n\n\tswitch o := obj.(type) {\n\tcase *Object:\n\t\to.Name = newName\n\t\to.Object.Name = newName\n\t\to.SetPath(newPath)\n\t\treturn o\n\tcase *model.Object:\n\t\to.Name = newName\n\t\to.SetPath(newPath)\n\t\treturn o\n\t}\n\n\tif setter, ok := obj.(model.SetPath); ok {\n\t\tsetter.SetPath(newPath)\n\t}\n\n\treturn &model.Object{\n\t\tID:       obj.GetID(),\n\t\tPath:     newPath,\n\t\tName:     newName,\n\t\tSize:     obj.GetSize(),\n\t\tModified: obj.ModTime(),\n\t\tCtime:    obj.CreateTime(),\n\t\tIsFolder: obj.IsDir(),\n\t\tHashInfo: obj.GetHash(),\n\t}\n}\n\nfunc parentPathOf(p string) string {\n\tif p == \"\" {\n\t\treturn \"\"\n\t}\n\tdir := path.Dir(p)\n\tif dir == \".\" {\n\t\treturn \"\"\n\t}\n\treturn dir\n}\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\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/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.Buffer{}\n\twriter := multipart.NewWriter(body)\n\tfilePart, err := writer.CreateFormFile(\"file\", file.GetName())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = utils.CopyWithBuffer(filePart, file)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = writer.WriteField(\"_token\", resp.Msg.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = writer.WriteField(\"puid\", fmt.Sprintf(\"%d\", 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\tr := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\tReader: body,\n\t\t\tSize:   int64(body.Len()),\n\t\t},\n\t\tUpdateProgress: up,\n\t})\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", \"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.Header.Set(\"Content-Length\", fmt.Sprintf(\"%d\", body.Len()))\n\tresps, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resps.Body.Close()\n\tbodys, err := io.ReadAll(resps.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar fileRsp UploadFileDataRsp\n\terr = json.Unmarshal(bodys, &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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:         false,\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/alist-org/alist/v3/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:       fmt.Sprintf(\"%d\", 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\"strings\"\n\n\t\"github.com/alist-org/alist/v3/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(\"POST\", \"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\", fmt.Sprintf(\"%d\", 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/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\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/alist-org/alist/v3/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},\n\t\tThumbnail: t,\n\t}\n}\n\ntype Config struct {\n\tLoginCaptcha bool   `json:\"loginCaptcha\"`\n\tCaptchaType  string `json:\"captcha_type\"`\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/cookie\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\tuploadUrl := u.UploadURLs[0]\n\tcredential := u.Credential\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"POST\", uploadUrl+\"?chunk=\"+strconv.Itoa(chunk),\n\t\t\tdriver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Authorization\", fmt.Sprint(credential))\n\t\treq.Header.Set(\"User-Agent\", d.getUA())\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\tif res.StatusCode != 200 {\n\t\t\t\treturn errors.New(res.Status)\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvar up Resp\n\t\t\terr = json.Unmarshal(body, &up)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif up.Code != 0 {\n\t\t\t\treturn errors.New(up.Msg)\n\t\t\t}\n\t\t\treturn nil\n\t\t}()\n\t\tif err == nil {\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\t\tchunk++\n\t\t} else {\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error: %s\", maxRetries, err)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[Cloudreve-Remote] server errors while uploading, retrying after %v...\", backoff)\n\t\t\ttime.Sleep(backoff)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {\n\tuploadUrl := u.UploadURLs[0]\n\tvar finish int64 = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"PUT\", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\tfinish += byteSize\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\tswitch {\n\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\tres.Body.Close()\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error %d\", maxRetries, res.StatusCode)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[Cloudreve-OneDrive] server errors %d while uploading, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\treturn errors.New(string(data))\n\t\tdefault:\n\t\t\tres.Body.Close()\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\t}\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\tvar finish int64 = 0\n\tvar chunk int = 0\n\tvar etags []string\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"PUT\", u.UploadURLs[chunk],\n\t\t\tdriver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\tfinish += byteSize\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tetag := res.Header.Get(\"ETag\")\n\t\tres.Body.Close()\n\t\tswitch {\n\t\tcase res.StatusCode != 200:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error %d\", maxRetries, res.StatusCode)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[Cloudreve-S3] server errors %d while uploading, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase etag == \"\":\n\t\t\treturn errors.New(\"faild to get ETag from header\")\n\t\tdefault:\n\t\t\tretryCount = 0\n\t\t\tetags = append(etags, etag)\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\t\tchunk++\n\t\t}\n\t}\n\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.NewRequest(\n\t\t\"POST\",\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype CloudreveV4 struct {\n\tmodel.Storage\n\tAddition\n\tref *CloudreveV4\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.AccessToken == \"\" && d.RefreshToken != \"\" {\n\t\treturn d.refreshToken()\n\t}\n\tif d.Username != \"\" {\n\t\treturn d.login()\n\t}\n\treturn nil\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\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 {\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: 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\tModified: src.UpdatedAt,\n\t\t\t\tCtime:    src.CreatedAt,\n\t\t\t\tIsFolder: src.Type == 1,\n\t\t\t},\n\t\t\tThumbnail: thumb,\n\t\t}, nil\n\t})\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/create\", 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}\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\treturn 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}, nil)\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)\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"cloudreve://my\",\n\tCheckStatus:       true,\n\tAlert:             \"\",\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/alist-org/alist/v3/internal/model\"\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\tAuthn        bool `json:\"authn\"`\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 Token struct {\n\tAccessToken    string    `json:\"access_token\"`\n\tRefreshToken   string    `json:\"refresh_token\"`\n\tAccessExpires  time.Time `json:\"access_expires\"`\n\tRefreshExpires time.Time `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      interface{} `json:\"metadata\"`\n\tPath          string      `json:\"path\"`\n\tCapability    string      `json:\"capability\"`\n\tOwned         bool        `json:\"owned\"`\n\tPrimaryEntity string      `json:\"primary_entity\"`\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 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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\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\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 == 401 && d.RefreshToken != \"\" && path != \"/session/token/refresh\" {\n\t\t\t// try to refresh token\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.request(method, path, callback, out)\n\t\t}\n\t\treturn errors.New(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) 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\tif !siteConfig.Authn {\n\t\treturn errors.New(\"authn not support\")\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\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *CloudreveV4) refreshToken() error {\n\tvar token Token\n\tif token.RefreshToken == \"\" {\n\t\tif d.Username != \"\" {\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\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\treturn err\n\t}\n\td.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken\n\top.MustSaveDriverStorage(d)\n\treturn nil\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\tuploadUrl := u.UploadUrls[0]\n\tcredential := u.Credential\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"POST\", uploadUrl+\"?chunk=\"+strconv.Itoa(chunk),\n\t\t\tdriver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Authorization\", fmt.Sprint(credential))\n\t\treq.Header.Set(\"User-Agent\", d.getUA())\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\tif res.StatusCode != 200 {\n\t\t\t\treturn errors.New(res.Status)\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvar up Resp\n\t\t\terr = json.Unmarshal(body, &up)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif up.Code != 0 {\n\t\t\t\treturn errors.New(up.Msg)\n\t\t\t}\n\t\t\treturn nil\n\t\t}()\n\t\tif err == nil {\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\t\tchunk++\n\t\t} else {\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error: %s\", maxRetries, err)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[Cloudreve-Remote] server errors while uploading, retrying after %v...\", backoff)\n\t\t\ttime.Sleep(backoff)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {\n\tuploadUrl := u.UploadUrls[0]\n\tvar finish int64 = 0\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, file.GetSize()))\n\t\treq.Header.Set(\"User-Agent\", d.getUA())\n\t\tfinish += byteSize\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\tswitch {\n\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\tres.Body.Close()\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error %d\", maxRetries, res.StatusCode)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[CloudreveV4-OneDrive] server errors %d while uploading, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\treturn errors.New(string(data))\n\t\tdefault:\n\t\t\tres.Body.Close()\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\t}\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) error {\n\tvar finish int64 = 0\n\tvar chunk int = 0\n\tvar etags []string\n\tDEFAULT := int64(u.ChunkSize)\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(http.MethodPut, u.UploadUrls[chunk],\n\t\t\tdriver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tetag := res.Header.Get(\"ETag\")\n\t\tres.Body.Close()\n\t\tswitch {\n\t\tcase res.StatusCode != 200:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors\", maxRetries)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"server error %d, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase etag == \"\":\n\t\t\treturn errors.New(\"faild to get ETag from header\")\n\t\tdefault:\n\t\t\tretryCount = 0\n\t\t\tetags = append(etags, etag)\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(file.GetSize()))\n\t\t\tchunk++\n\t\t}\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.NewRequest(\n\t\t\"POST\",\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\treturn d.request(http.MethodPost, \"/callback/s3/\"+u.SessionID+\"/\"+u.CallbackSecret, func(req *resty.Request) {\n\t\treq.SetBody(\"{}\")\n\t}, nil)\n}\n"
  },
  {
    "path": "drivers/crypt/driver.go",
    "content": "package crypt\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\tremoteStorage driver.Driver\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\n\top.MustSaveDriverStorage(d)\n\n\t//need remote storage exist\n\tstorage, err := fs.GetStorage(d.RemotePath, &fs.GetStoragesArgs{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"can't find remote storage: %w\", err)\n\t}\n\td.remoteStorage = storage\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\tpath := dir.GetPath()\n\t//return d.list(ctx, d.RemotePath, path)\n\t//remoteFull\n\n\tobjs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true})\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\tvar result []model.Obj\n\tfor _, obj := range objs {\n\t\tif obj.IsDir() {\n\t\t\tname, err := d.cipher.DecryptDirName(obj.GetName())\n\t\t\tif err != nil {\n\t\t\t\t//filter illegal files\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !d.ShowHidden && strings.HasPrefix(name, \".\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tobjRes := model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tSize:     0,\n\t\t\t\tModified: obj.ModTime(),\n\t\t\t\tIsFolder: obj.IsDir(),\n\t\t\t\tCtime:    obj.CreateTime(),\n\t\t\t\t// discarding hash as it's encrypted\n\t\t\t}\n\t\t\tresult = append(result, &objRes)\n\t\t} else {\n\t\t\tthumb, ok := model.GetThumb(obj)\n\t\t\tsize, err := d.cipher.DecryptedSize(obj.GetSize())\n\t\t\tif err != nil {\n\t\t\t\t//filter illegal files\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname, err := d.cipher.DecryptFileName(obj.GetName())\n\t\t\tif err != nil {\n\t\t\t\t//filter illegal files\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !d.ShowHidden && strings.HasPrefix(name, \".\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tobjRes := model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tSize:     size,\n\t\t\t\tModified: obj.ModTime(),\n\t\t\t\tIsFolder: obj.IsDir(),\n\t\t\t\tCtime:    obj.CreateTime(),\n\t\t\t\t// discarding hash as it's encrypted\n\t\t\t}\n\t\t\tif d.Thumbnail && thumb == \"\" {\n\t\t\t\tthumbPath := stdpath.Join(args.ReqPath, \".thumbnails\", name+\".webp\")\n\t\t\t\tthumb = fmt.Sprintf(\"%s/d%s?sign=%s\",\n\t\t\t\t\tcommon.GetApiUrl(common.GetHttpReq(ctx)),\n\t\t\t\t\tutils.EncodePath(thumbPath, true),\n\t\t\t\t\tsign.Sign(thumbPath))\n\t\t\t}\n\t\t\tif !ok && !d.Thumbnail {\n\t\t\t\tresult = append(result, &objRes)\n\t\t\t} else {\n\t\t\t\tobjWithThumb := 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\tresult = append(result, &objWithThumb)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) {\n\tif utils.PathEqual(path, \"/\") {\n\t\treturn &model.Object{\n\t\t\tName:     \"Root\",\n\t\t\tIsFolder: true,\n\t\t\tPath:     \"/\",\n\t\t}, nil\n\t}\n\tremoteFullPath := \"\"\n\tvar remoteObj model.Obj\n\tvar err, err2 error\n\tfirstTryIsFolder, secondTry := guessPath(path)\n\tremoteFullPath = d.getPathForRemote(path, firstTryIsFolder)\n\tremoteObj, err = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\tif errs.IsObjectNotFound(err) && secondTry {\n\t\t\t//try the opposite\n\t\t\tremoteFullPath = d.getPathForRemote(path, !firstTryIsFolder)\n\t\t\tremoteObj, err2 = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})\n\t\t\tif err2 != nil {\n\t\t\t\treturn nil, err2\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tvar size int64 = 0\n\tname := \"\"\n\tif !remoteObj.IsDir() {\n\t\tsize, err = d.cipher.DecryptedSize(remoteObj.GetSize())\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"DecryptedSize failed for %s ,will use original size, err:%s\", path, err)\n\t\t\tsize = remoteObj.GetSize()\n\t\t}\n\t\tname, err = d.cipher.DecryptFileName(remoteObj.GetName())\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"DecryptFileName failed for %s ,will use original name, err:%s\", path, err)\n\t\t\tname = remoteObj.GetName()\n\t\t}\n\t} else {\n\t\tname, err = d.cipher.DecryptDirName(remoteObj.GetName())\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"DecryptDirName failed for %s ,will use original name, err:%s\", path, err)\n\t\t\tname = remoteObj.GetName()\n\t\t}\n\t}\n\tobj := &model.Object{\n\t\tPath:     path,\n\t\tName:     name,\n\t\tSize:     size,\n\t\tModified: remoteObj.ModTime(),\n\t\tIsFolder: remoteObj.IsDir(),\n\t}\n\treturn obj, nil\n\t//return nil, errs.ObjectNotFound\n}\n\nfunc (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdstDirActualPath, err := d.getActualPathForRemote(file.GetPath(), false)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\tremoteLink, remoteFile, err := op.Link(ctx, d.remoteStorage, dstDirActualPath, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif remoteLink.RangeReadCloser == nil && remoteLink.MFile == nil && len(remoteLink.URL) == 0 {\n\t\treturn nil, fmt.Errorf(\"the remote storage driver need to be enhanced to support encrytion\")\n\t}\n\tremoteFileSize := remoteFile.GetSize()\n\tremoteClosers := utils.EmptyClosers()\n\trangeReaderFunc := func(ctx context.Context, underlyingOffset, underlyingLength int64) (io.ReadCloser, error) {\n\t\tlength := underlyingLength\n\t\tif underlyingLength >= 0 && underlyingOffset+underlyingLength >= remoteFileSize {\n\t\t\tlength = -1\n\t\t}\n\t\trrc := remoteLink.RangeReadCloser\n\t\tif len(remoteLink.URL) > 0 {\n\t\t\tvar converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, remoteLink)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trrc = converted\n\t\t}\n\t\tif rrc != nil {\n\t\t\tremoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: underlyingOffset, Length: length})\n\t\t\tremoteClosers.AddClosers(rrc.GetClosers())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn remoteReader, nil\n\t\t}\n\t\tif remoteLink.MFile != nil {\n\t\t\t_, err := remoteLink.MFile.Seek(underlyingOffset, io.SeekStart)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t//keep reuse same MFile and close at last.\n\t\t\tremoteClosers.Add(remoteLink.MFile)\n\t\t\treturn io.NopCloser(remoteLink.MFile), nil\n\t\t}\n\n\t\treturn nil, errs.NotSupport\n\n\t}\n\tresultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\treadSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn readSeeker, nil\n\t}\n\n\tresultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}\n\tresultLink := &model.Link{\n\t\tRangeReadCloser: resultRangeReadCloser,\n\t\tExpiration:      remoteLink.Expiration,\n\t}\n\n\treturn resultLink, nil\n\n}\n\nfunc (d *Crypt) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tdstDirActualPath, err := d.getActualPathForRemote(parentDir.GetPath(), true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\tdir := d.cipher.EncryptDirName(dirName)\n\treturn op.MakeDir(ctx, d.remoteStorage, stdpath.Join(dstDirActualPath, dir))\n}\n\nfunc (d *Crypt) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcRemoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\tdstRemoteActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), dstDir.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\treturn op.Move(ctx, d.remoteStorage, srcRemoteActualPath, dstRemoteActualPath)\n}\n\nfunc (d *Crypt) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tremoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", 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, d.remoteStorage, remoteActualPath, newEncryptedName)\n}\n\nfunc (d *Crypt) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcRemoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\tdstRemoteActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), dstDir.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\treturn op.Copy(ctx, d.remoteStorage, srcRemoteActualPath, dstRemoteActualPath)\n\n}\n\nfunc (d *Crypt) Remove(ctx context.Context, obj model.Obj) error {\n\tremoteActualPath, err := d.getActualPathForRemote(obj.GetPath(), obj.IsDir())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", err)\n\t}\n\treturn op.Remove(ctx, d.remoteStorage, remoteActualPath)\n}\n\nfunc (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {\n\tdstDirActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert path to remote path: %w\", 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\tWebPutAsTask:      streamer.NeedStore(),\n\t\tForceStreamUpload: true,\n\t\tExist:             streamer.GetExist(),\n\t}\n\terr = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n//func (d *Safe) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Crypt)(nil)\n"
  },
  {
    "path": "drivers/crypt/meta.go",
    "content": "package crypt\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\t//driver.RootPath\n\t//driver.RootID\n\t// define other\n\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\tOnlyLocal:         false,\n\tOnlyProxy:         true,\n\tNoCache:           true,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\t\"github.com/alist-org/alist/v3/internal/op\"\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.Index(path[lastSlash:], \".\") < 0 {\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) getPathForRemote(path string, isFolder bool) (remoteFullPath string) {\n\tif isFolder && !strings.HasSuffix(path, \"/\") {\n\t\tpath = path + \"/\"\n\t}\n\tdir, fileName := filepath.Split(path)\n\n\tremoteDir := d.cipher.EncryptDirName(dir)\n\tremoteFileName := \"\"\n\tif len(strings.TrimSpace(fileName)) > 0 {\n\t\tremoteFileName = d.cipher.EncryptFileName(fileName)\n\t}\n\treturn stdpath.Join(d.RemotePath, remoteDir, remoteFileName)\n\n}\n\n// actual path is used for internal only. any link for user should come from remoteFullPath\nfunc (d *Crypt) getActualPathForRemote(path string, isFolder bool) (string, error) {\n\t_, remoteActualPath, err := op.GetStorageAndActualPath(d.getPathForRemote(path, isFolder))\n\treturn remoteActualPath, err\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/google/uuid\"\n)\n\ntype Doubao struct {\n\tmodel.Storage\n\tAddition\n\t*UploadToken\n\tUserId       string\n\tuploadThread int\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\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\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\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 := 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\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\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\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\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\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(&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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\n\nvar config = driver.Config{\n\tName:              \"Doubao\",\n\tLocalSort:         true,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Doubao{}\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/alist-org/alist/v3/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 int       `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        int                   `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\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/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\t\"hash/crc32\"\n\t\"io\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\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        = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36\"\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\": filepath.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(config *UploadConfig, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, dataType string) (model.Obj, error) {\n\tdata, err := io.ReadAll(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 计算CRC32\n\tcrc32Hash := crc32.NewIEEE()\n\tcrc32Hash.Write(data)\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\n\tuploadResp := UploadResp{}\n\n\tif _, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Content-Type\":        \"application/octet-stream\",\n\t\t\t\"Content-Crc32\":       crc32Value,\n\t\t\t\"Content-Length\":      fmt.Sprintf(\"%d\", len(data)),\n\t\t\t\"Content-Disposition\": fmt.Sprintf(\"attachment; filename=%s\", url.QueryEscape(storeInfo.StoreURI)),\n\t\t})\n\n\t\treq.SetBody(data)\n\t}, &uploadResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif uploadResp.Code != 2000 {\n\t\treturn nil, fmt.Errorf(\"upload failed: %s\", uploadResp.Message)\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\ttotalParts := (fileSize + chunkSize - 1) / chunkSize\n\t// 创建分片信息组\n\tparts := make([]UploadPart, totalParts)\n\t// 缓存文件\n\ttempFile, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to cache file: %w\", err)\n\t}\n\tdefer tempFile.Close()\n\tup(10.0) // 更新进度\n\t// 设置并行上传\n\tthreadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,\n\t\tretry.Attempts(1),\n\t\tretry.Delay(time.Second),\n\t\tretry.DelayType(retry.BackOffDelay))\n\n\tvar partsMutex sync.Mutex\n\t// 并行上传所有分片\n\tfor partIndex := int64(0); partIndex < totalParts; partIndex++ {\n\t\tif utils.IsCanceled(uploadCtx) {\n\t\t\tbreak\n\t\t}\n\t\tpartIndex := partIndex\n\t\tpartNumber := partIndex + 1 // 分片编号从1开始\n\n\t\tthreadG.Go(func(ctx context.Context) error {\n\t\t\t// 计算此分片的大小和偏移\n\t\t\toffset := partIndex * chunkSize\n\t\t\tsize := chunkSize\n\t\t\tif partIndex == totalParts-1 {\n\t\t\t\tsize = fileSize - offset\n\t\t\t}\n\n\t\t\tlimitedReader := driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, size))\n\t\t\t// 读取数据到内存\n\t\t\tdata, err := io.ReadAll(limitedReader)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to read part %d: %w\", partNumber, err)\n\t\t\t}\n\t\t\t// 计算CRC32\n\t\t\tcrc32Value := calculateCRC32(data)\n\t\t\t// 使用_retryOperation上传分片\n\t\t\tvar uploadPart UploadPart\n\t\t\tif err = d._retryOperation(fmt.Sprintf(\"Upload part %d\", partNumber), func() error {\n\t\t\t\tvar err error\n\t\t\t\tuploadPart, err = d.uploadPart(config, uploadUrl, uploadID, partNumber, data, crc32Value)\n\t\t\t\treturn err\n\t\t\t}); err != nil {\n\t\t\t\treturn fmt.Errorf(\"part %d upload failed: %w\", partNumber, err)\n\t\t\t}\n\t\t\t// 记录成功上传的分片\n\t\t\tpartsMutex.Lock()\n\t\t\tparts[partIndex] = UploadPart{\n\t\t\t\tPartNumber: strconv.FormatInt(partNumber, 10),\n\t\t\t\tEtag:       uploadPart.Etag,\n\t\t\t\tCrc32:      crc32Value,\n\t\t\t}\n\t\t\tpartsMutex.Unlock()\n\t\t\t// 更新进度\n\t\t\tprogress := 10.0 + 90.0*float64(threadG.Success()+1)/float64(totalParts)\n\t\t\tup(math.Min(progress, 95.0))\n\n\t\t\treturn nil\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) uploadPart(config *UploadConfig, uploadUrl, uploadID string, partNumber int64, data []byte, crc32Value string) (resp UploadPart, err error) {\n\tuploadResp := UploadResp{}\n\tstoreInfo := config.InnerUploadAddress.UploadNodes[0].StoreInfos[0]\n\n\t_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {\n\t\treq.SetHeaders(map[string]string{\n\t\t\t\"Content-Type\":        \"application/octet-stream\",\n\t\t\t\"Content-Crc32\":       crc32Value,\n\t\t\t\"Content-Length\":      fmt.Sprintf(\"%d\", len(data)),\n\t\t\t\"Content-Disposition\": fmt.Sprintf(\"attachment; filename=%s\", url.QueryEscape(storeInfo.StoreURI)),\n\t\t})\n\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"uploadid\":    uploadID,\n\t\t\t\"part_number\": strconv.FormatInt(partNumber, 10),\n\t\t\t\"phase\":       \"transfer\",\n\t\t})\n\n\t\treq.SetBody(data)\n\t\treq.SetContentLength(true)\n\t}, &uploadResp)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tif uploadResp.Code != 2000 {\n\t\treturn resp, fmt.Errorf(\"upload part failed: %s\", uploadResp.Message)\n\t} else if uploadResp.Data.Crc32 != crc32Value {\n\t\treturn resp, fmt.Errorf(\"upload part failed: crc32 mismatch, expected %s, got %s\", crc32Value, uploadResp.Data.Crc32)\n\t}\n\n\treturn uploadResp.Data, 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// 计算CRC32\nfunc calculateCRC32(data []byte) string {\n\thash := crc32.NewIEEE()\n\thash.Write(data)\n\treturn hex.EncodeToString(hash.Sum(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\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 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_new/driver.go",
    "content": "package doubao_new\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\ntype DoubaoNew struct {\n\tmodel.Storage\n\tAddition\n\tTtLogid string\n}\n\nfunc (d *DoubaoNew) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *DoubaoNew) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *DoubaoNew) Init(ctx context.Context) error {\n\t// TODO login / refresh token\n\t//op.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *DoubaoNew) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *DoubaoNew) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tnodes, err := d.listAllChildren(ctx, dir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tobjs := make([]model.Obj, 0, len(nodes))\n\tfor _, node := range nodes {\n\t\tsize := parseSize(node.Extra.Size)\n\t\tisFolder := node.Type == 0\n\t\tobj := &Object{\n\t\t\tObject: model.Object{\n\t\t\t\tID:       node.NodeToken,\n\t\t\t\tPath:     dir.GetID(),\n\t\t\t\tName:     node.Name,\n\t\t\t\tSize:     size,\n\t\t\t\tModified: time.Unix(node.EditTime, 0),\n\t\t\t\tCtime:    time.Unix(node.CreateTime, 0),\n\t\t\t\tIsFolder: isFolder,\n\t\t\t},\n\t\t\tObjToken: node.ObjToken,\n\t\t\tNodeType: node.NodeType,\n\t\t\tObjType:  node.Type,\n\t\t\tURL:      node.URL,\n\t\t}\n\t\tobjs = append(objs, obj)\n\t}\n\n\treturn objs, nil\n}\n\nfunc (d *DoubaoNew) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tobj, ok := file.(*Object)\n\tif !ok {\n\t\treturn nil, errors.New(\"unsupported object type\")\n\t}\n\tif obj.IsFolder {\n\t\treturn nil, errs.LinkIsDir\n\t}\n\tif args.Type == \"preview\" || args.Type == \"thumb\" {\n\t\tif link, err := d.previewLink(ctx, obj, args); err == nil {\n\t\t\treturn link, nil\n\t\t}\n\t}\n\tauth := d.resolveAuthorization()\n\tdpop := d.resolveDpop()\n\tif auth == \"\" || dpop == \"\" {\n\t\treturn nil, errors.New(\"missing authorization or dpop\")\n\t}\n\tif obj.ObjToken == \"\" {\n\t\treturn nil, errors.New(\"missing obj_token\")\n\t}\n\n\tquery := url.Values{}\n\tquery.Set(\"authorization\", auth)\n\tquery.Set(\"dpop\", dpop)\n\n\tdownloadURL := DownloadBaseURL + \"/space/api/box/stream/download/all/\" + obj.ObjToken + \"/?\" + query.Encode()\n\n\theaders := http.Header{\n\t\t\"Referer\":    []string{\"https://www.doubao.com/\"},\n\t\t\"User-Agent\": []string{base.UserAgent},\n\t}\n\n\treturn &model.Link{\n\t\tURL:    downloadURL,\n\t\tHeader: headers,\n\t}, nil\n}\n\nfunc (d *DoubaoNew) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tnode, err := d.createFolder(ctx, parentDir.GetID(), dirName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Object{\n\t\tObject: model.Object{\n\t\t\tID:       node.NodeToken,\n\t\t\tPath:     parentDir.GetID(),\n\t\t\tName:     node.Name,\n\t\t\tSize:     parseSize(node.Extra.Size),\n\t\t\tModified: time.Unix(node.EditTime, 0),\n\t\t\tCtime:    time.Unix(node.CreateTime, 0),\n\t\t\tIsFolder: true,\n\t\t},\n\t\tObjToken: node.ObjToken,\n\t\tNodeType: node.NodeType,\n\t\tObjType:  node.Type,\n\t\tURL:      node.URL,\n\t}, nil\n}\n\nfunc (d *DoubaoNew) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif srcObj == nil {\n\t\treturn nil, errors.New(\"nil source object\")\n\t}\n\tif dstDir == nil {\n\t\treturn nil, errors.New(\"nil destination dir\")\n\t}\n\tsrcToken := srcObj.GetID()\n\tif srcToken == \"\" {\n\t\tif obj, ok := srcObj.(*Object); ok {\n\t\t\tsrcToken = obj.ObjToken\n\t\t}\n\t}\n\tif srcToken == \"\" {\n\t\treturn nil, errors.New(\"missing source token\")\n\t}\n\tif err := d.moveObj(ctx, srcToken, dstDir.GetID()); err != nil {\n\t\treturn nil, err\n\t}\n\tif obj, ok := srcObj.(*Object); ok {\n\t\tclone := *obj\n\t\tclone.Path = dstDir.GetID()\n\t\treturn &clone, nil\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *DoubaoNew) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tif srcObj == nil {\n\t\treturn nil, errors.New(\"nil source object\")\n\t}\n\tif srcObj.IsDir() {\n\t\tif err := d.renameFolder(ctx, srcObj.GetID(), newName); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tfileToken := \"\"\n\t\tif obj, ok := srcObj.(*Object); ok {\n\t\t\tfileToken = obj.ObjToken\n\t\t}\n\t\tif fileToken == \"\" {\n\t\t\tfileToken = srcObj.GetID()\n\t\t}\n\t\tif err := d.renameFile(ctx, fileToken, newName); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif obj, ok := srcObj.(*Object); ok {\n\t\tclone := *obj\n\t\tclone.Name = newName\n\t\treturn &clone, nil\n\t}\n\treturn srcObj, nil\n}\n\nfunc (d *DoubaoNew) 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 *DoubaoNew) Remove(ctx context.Context, obj model.Obj) error {\n\tif obj == nil {\n\t\treturn errors.New(\"nil object\")\n\t}\n\ttoken := obj.GetID()\n\tif token == \"\" {\n\t\tif o, ok := obj.(*Object); ok {\n\t\t\ttoken = o.ObjToken\n\t\t}\n\t}\n\tif token == \"\" {\n\t\treturn errors.New(\"missing object token\")\n\t}\n\treturn d.removeObj(ctx, []string{token})\n}\n\nfunc (d *DoubaoNew) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tif file == nil {\n\t\treturn nil, errors.New(\"nil file\")\n\t}\n\tif file.GetSize() <= 0 {\n\t\treturn nil, errors.New(\"invalid file size\")\n\t}\n\n\tuploadPrep, err := d.prepareUpload(ctx, file.GetName(), file.GetSize(), dstDir.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif uploadPrep.BlockSize <= 0 {\n\t\treturn nil, errors.New(\"invalid block size from prepare\")\n\t}\n\n\ttmpFile, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tmpFile.Close()\n\n\tblockSize := uploadPrep.BlockSize\n\ttotalSize := file.GetSize()\n\tnumBlocks := int((totalSize + blockSize - 1) / blockSize)\n\tblocks := make([]UploadBlock, 0, numBlocks)\n\tblockMeta := make(map[int]UploadBlock, numBlocks)\n\n\tfor seq := 0; seq < numBlocks; seq++ {\n\t\toffset := int64(seq) * blockSize\n\t\tlength := blockSize\n\t\tif remain := totalSize - offset; remain < length {\n\t\t\tlength = remain\n\t\t}\n\t\tbuf := make([]byte, int(length))\n\t\tn, err := tmpFile.ReadAt(buf, offset)\n\t\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\t\treturn nil, err\n\t\t}\n\t\tbuf = buf[:n]\n\t\tsum := sha256.Sum256(buf)\n\t\thash := base64.StdEncoding.EncodeToString(sum[:])\n\t\tchecksum := adler32String(buf)\n\n\t\tblock := UploadBlock{\n\t\t\tHash:       hash,\n\t\t\tSeq:        seq,\n\t\t\tSize:       int64(n),\n\t\t\tChecksum:   checksum,\n\t\t\tIsUploaded: true,\n\t\t}\n\t\tblocks = append(blocks, block)\n\t\tblockMeta[seq] = block\n\t}\n\n\tneeded, err := d.uploadBlocks(ctx, uploadPrep.UploadID, blocks, \"explorer\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(needed.NeededUploadBlocks) > 0 {\n\t\tsort.Slice(needed.NeededUploadBlocks, func(i, j int) bool {\n\t\t\treturn needed.NeededUploadBlocks[i].Seq < needed.NeededUploadBlocks[j].Seq\n\t\t})\n\t\tconst maxMergeBlockCount = 20\n\t\tvar (\n\t\t\tgroupSeqs      []int\n\t\t\tgroupChecksums []string\n\t\t\tgroupSizes     []int64\n\t\t\tgroupRealSize  int64\n\t\t\tgroupExpectSum int64\n\t\t\tgroupBuf       bytes.Buffer\n\t\t\tuploadedBytes  int64\n\t\t)\n\n\t\tflushGroup := func() error {\n\t\t\tif len(groupSeqs) == 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdata := groupBuf.Bytes()\n\t\t\texpectLen := groupExpectSum\n\t\t\tif len(data) > 0 {\n\t\t\t\theadLen := 32\n\t\t\t\tif len(data) < headLen {\n\t\t\t\t\theadLen = len(data)\n\t\t\t\t}\n\t\t\t\ttailLen := 32\n\t\t\t\tif len(data) < tailLen {\n\t\t\t\t\ttailLen = len(data)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif int64(len(data)) != expectLen {\n\t\t\t\treturn fmt.Errorf(\"[doubao_new] merge blocks invalid body len: got=%d expect=%d seqs=%v\", len(data), expectLen, groupSeqs)\n\t\t\t}\n\t\t\tmergeResp, err := d.mergeUploadBlocks(ctx, uploadPrep.UploadID, groupSeqs, groupChecksums, groupSizes, blockSize, data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(mergeResp.SuccessSeqList) != len(groupSeqs) {\n\t\t\t\treturn fmt.Errorf(\"[doubao_new] merge blocks incomplete: %v\", mergeResp.SuccessSeqList)\n\t\t\t}\n\t\t\tsuccess := make(map[int]bool, len(mergeResp.SuccessSeqList))\n\t\t\tfor _, seq := range mergeResp.SuccessSeqList {\n\t\t\t\tsuccess[seq] = true\n\t\t\t}\n\t\t\tfor _, seq := range groupSeqs {\n\t\t\t\tif !success[seq] {\n\t\t\t\t\treturn fmt.Errorf(\"[doubao_new] merge blocks missing seq %d\", seq)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tuploadedBytes += groupRealSize\n\t\t\tgroupSeqs = groupSeqs[:0]\n\t\t\tgroupChecksums = groupChecksums[:0]\n\t\t\tgroupSizes = groupSizes[:0]\n\t\t\tgroupRealSize = 0\n\t\t\tgroupExpectSum = 0\n\t\t\tgroupBuf.Reset()\n\t\t\tif up != nil {\n\t\t\t\tpercent := float64(uploadedBytes) / float64(totalSize) * 100\n\t\t\t\tup(percent)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tfor _, item := range needed.NeededUploadBlocks {\n\t\t\tif _, ok := blockMeta[item.Seq]; !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"[doubao_new] missing block meta for seq %d\", item.Seq)\n\t\t\t}\n\t\t\tif item.Size <= 0 {\n\t\t\t\treturn nil, fmt.Errorf(\"[doubao_new] invalid block size from needed list: seq=%d size=%d\", item.Seq, item.Size)\n\t\t\t}\n\t\t\toffset := int64(item.Seq) * blockSize\n\t\t\tbuf := make([]byte, int(item.Size))\n\t\t\tn, err := tmpFile.ReadAt(buf, offset)\n\t\t\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif n != len(buf) {\n\t\t\t\treturn nil, fmt.Errorf(\"[doubao_new] short read: seq=%d want=%d got=%d\", item.Seq, len(buf), n)\n\t\t\t}\n\t\t\tbuf = buf[:n]\n\t\t\trealAdler := adler32String(buf)\n\t\t\tif realAdler != item.Checksum {\n\t\t\t\treturn nil, fmt.Errorf(\"[doubao_new] block checksum mismatch: seq=%d offset=%d adler32=%s step2=%s\", item.Seq, offset, realAdler, item.Checksum)\n\t\t\t}\n\t\t\tpayloadStart := groupBuf.Len()\n\t\t\tgroupBuf.Write(buf)\n\t\t\tpayloadEnd := groupBuf.Len()\n\t\t\tpayloadAdler := adler32String(groupBuf.Bytes()[payloadStart:payloadEnd])\n\t\t\tif payloadAdler != item.Checksum {\n\t\t\t\treturn nil, fmt.Errorf(\"[doubao_new] payload checksum mismatch: seq=%d start=%d end=%d adler32=%s step2=%s\", item.Seq, payloadStart, payloadEnd, payloadAdler, item.Checksum)\n\t\t\t}\n\t\t\tgroupSeqs = append(groupSeqs, item.Seq)\n\t\t\tgroupChecksums = append(groupChecksums, item.Checksum)\n\t\t\tgroupSizes = append(groupSizes, item.Size)\n\t\t\tgroupRealSize += int64(n)\n\t\t\tgroupExpectSum += item.Size\n\t\t\tif len(groupSeqs) >= maxMergeBlockCount {\n\t\t\t\tif err := flushGroup(); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif err := flushGroup(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif up != nil {\n\t\t\tup(100)\n\t\t}\n\t} else if up != nil {\n\t\tup(100)\n\t}\n\n\tnumBlocksFinish := uploadPrep.NumBlocks\n\tif numBlocksFinish <= 0 {\n\t\tnumBlocksFinish = numBlocks\n\t}\n\tfinish, err := d.finishUpload(ctx, uploadPrep.UploadID, numBlocksFinish, \"explorer\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnodeToken := finish.Extra.NodeToken\n\tif nodeToken == \"\" {\n\t\tnodeToken = finish.FileToken\n\t}\n\tnow := time.Now()\n\treturn &Object{\n\t\tObject: model.Object{\n\t\t\tID:       nodeToken,\n\t\t\tPath:     dstDir.GetID(),\n\t\t\tName:     file.GetName(),\n\t\t\tSize:     file.GetSize(),\n\t\t\tModified: now,\n\t\t\tCtime:    now,\n\t\t\tIsFolder: false,\n\t\t},\n\t\tObjToken: finish.FileToken,\n\t}, nil\n}\n\nfunc (d *DoubaoNew) 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 *DoubaoNew) 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 *DoubaoNew) 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 *DoubaoNew) 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 *DoubaoNew) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tswitch args.Method {\n\tcase \"doubao_preview\", \"preview\":\n\t\tobj, ok := args.Obj.(*Object)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"unsupported object type\")\n\t\t}\n\t\tinfo, err := d.getFileInfo(ctx, obj.ObjToken)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentry, ok := info.PreviewMeta.Data[\"22\"]\n\t\tif !ok || entry.Status != 0 {\n\t\t\treturn nil, errs.NotSupport\n\t\t}\n\n\t\timgExt := \".webp\"\n\t\tpageNums := 1\n\t\tif entry.Extra != \"\" {\n\t\t\tvar extra PreviewImageExtra\n\t\t\tif err := json.Unmarshal([]byte(entry.Extra), &extra); err == nil {\n\t\t\t\tif extra.ImgExt != \"\" {\n\t\t\t\t\timgExt = extra.ImgExt\n\t\t\t\t}\n\t\t\t\tif extra.PageNums > 0 {\n\t\t\t\t\tpageNums = extra.PageNums\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn base.Json{\n\t\t\t\"version\":   info.Version,\n\t\t\t\"img_ext\":   imgExt,\n\t\t\t\"page_nums\": pageNums,\n\t\t}, nil\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n}\n\nfunc (d *DoubaoNew) listAllChildren(ctx context.Context, parentToken string) ([]Node, error) {\n\tnodes := make([]Node, 0, 50)\n\tlastLabel := \"\"\n\tfor page := 0; page < 100; page++ {\n\t\tdata, err := d.listChildren(ctx, parentToken, lastLabel)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(data.NodeList) > 0 {\n\t\t\tfor _, token := range data.NodeList {\n\t\t\t\tnode, ok := data.Entities.Nodes[token]\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnodes = append(nodes, node)\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, node := range data.Entities.Nodes {\n\t\t\t\tnodes = append(nodes, node)\n\t\t\t}\n\t\t}\n\n\t\tif !data.HasMore || data.LastLabel == \"\" || data.LastLabel == lastLabel {\n\t\t\tbreak\n\t\t}\n\t\tlastLabel = data.LastLabel\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn nodes, nil\n}\n\nfunc (d *DoubaoNew) previewLink(ctx context.Context, obj *Object, args model.LinkArgs) (*model.Link, error) {\n\tauth := d.resolveAuthorization()\n\tdpop := d.resolveDpop()\n\tif auth == \"\" || dpop == \"\" {\n\t\treturn nil, errors.New(\"missing authorization or dpop\")\n\t}\n\tif obj.ObjToken == \"\" {\n\t\treturn nil, errors.New(\"missing obj_token\")\n\t}\n\tinfo, err := d.getFileInfo(ctx, obj.ObjToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tentry, ok := info.PreviewMeta.Data[\"22\"]\n\tif !ok || entry.Status != 0 {\n\t\treturn nil, errors.New(\"preview not available\")\n\t}\n\n\tsubID := \"\"\n\tpageIndex := 0\n\tif args.HttpReq != nil {\n\t\tquery := args.HttpReq.URL.Query()\n\t\tif v := query.Get(\"sub_id\"); v != \"\" {\n\t\t\tsubID = v\n\t\t} else if v := query.Get(\"page\"); v != \"\" {\n\t\t\tif p, err := strconv.Atoi(v); err == nil && p >= 0 {\n\t\t\t\tpageIndex = p\n\t\t\t}\n\t\t}\n\t}\n\tif subID == \"\" {\n\t\timgExt := \".webp\"\n\t\tpageNums := 0\n\t\tif entry.Extra != \"\" {\n\t\t\tvar extra PreviewImageExtra\n\t\t\tif err := json.Unmarshal([]byte(entry.Extra), &extra); err == nil {\n\t\t\t\tif extra.ImgExt != \"\" {\n\t\t\t\t\timgExt = extra.ImgExt\n\t\t\t\t}\n\t\t\t\tpageNums = extra.PageNums\n\t\t\t}\n\t\t}\n\t\tif pageNums > 0 && pageIndex >= pageNums {\n\t\t\tpageIndex = pageNums - 1\n\t\t}\n\t\tsubID = fmt.Sprintf(\"img_%d%s\", pageIndex, imgExt)\n\t}\n\n\tquery := url.Values{}\n\tquery.Set(\"preview_type\", \"22\")\n\tquery.Set(\"sub_id\", subID)\n\tif info.Version != \"\" {\n\t\tquery.Set(\"version\", info.Version)\n\t}\n\tpreviewURL := fmt.Sprintf(\"%s/space/api/box/stream/download/preview_sub/%s?%s\", BaseURL, obj.ObjToken, query.Encode())\n\n\theaders := http.Header{\n\t\t\"Referer\":       []string{\"https://www.doubao.com/\"},\n\t\t\"User-Agent\":    []string{base.UserAgent},\n\t\t\"Authorization\": []string{auth},\n\t\t\"Dpop\":          []string{dpop},\n\t}\n\n\treturn &model.Link{\n\t\tURL:    previewURL,\n\t\tHeader: headers,\n\t}, nil\n}\n\nfunc parseSize(size string) int64 {\n\tif size == \"\" {\n\t\treturn 0\n\t}\n\tval, err := strconv.ParseInt(size, 10, 64)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn val\n}\n\nvar _ driver.Driver = (*DoubaoNew)(nil)\n"
  },
  {
    "path": "drivers/doubao_new/meta.go",
    "content": "package doubao_new\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootID\n\t// define other\n\tAuthorization string `json:\"authorization\" help:\"DPoP access token (Authorization header value); optional if present in cookie\"`\n\tDpop          string `json:\"dpop\" help:\"DPoP header value; optional if present in cookie\"`\n\tCookie        string `json:\"cookie\" help:\"Optional cookie; only used to extract authorization/dpop tokens\"`\n\tDebug         bool   `json:\"debug\" help:\"Enable debug logs for upload\"`\n}\n\nvar config = driver.Config{\n\tName:              \"DoubaoNew\",\n\tLocalSort:         true,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &DoubaoNew{}\n\t})\n}\n"
  },
  {
    "path": "drivers/doubao_new/types.go",
    "content": "package doubao_new\n\nimport \"github.com/alist-org/alist/v3/internal/model\"\n\ntype BaseResp struct {\n\tCode    int    `json:\"code\"`\n\tMsg     string `json:\"msg,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\ntype ListResp struct {\n\tBaseResp\n\tData ListData `json:\"data\"`\n}\n\ntype ListData struct {\n\tHasMore   bool     `json:\"has_more\"`\n\tLastLabel string   `json:\"last_label\"`\n\tNodeList  []string `json:\"node_list\"`\n\tEntities  struct {\n\t\tNodes map[string]Node `json:\"nodes\"`\n\t\tUsers map[string]User `json:\"users\"`\n\t} `json:\"entities\"`\n}\n\ntype Node struct {\n\tToken      string `json:\"token\"`\n\tNodeToken  string `json:\"node_token\"`\n\tObjToken   string `json:\"obj_token\"`\n\tName       string `json:\"name\"`\n\tType       int    `json:\"type\"`\n\tNodeType   int    `json:\"node_type\"`\n\tOwnerID    string `json:\"owner_id\"`\n\tEditUID    string `json:\"edit_uid\"`\n\tCreateTime int64  `json:\"create_time\"`\n\tEditTime   int64  `json:\"edit_time\"`\n\tURL        string `json:\"url\"`\n\tExtra      struct {\n\t\tSize string `json:\"size\"`\n\t} `json:\"extra\"`\n}\n\ntype User struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype Object struct {\n\tmodel.Object\n\tObjToken string\n\tNodeType int\n\tObjType  int\n\tURL      string\n}\n\ntype CreateFolderResp struct {\n\tBaseResp\n\tData struct {\n\t\tEntities struct {\n\t\t\tNodes map[string]Node `json:\"nodes\"`\n\t\t} `json:\"entities\"`\n\t\tNodeList []string `json:\"node_list\"`\n\t} `json:\"data\"`\n}\n\ntype FileInfoResp struct {\n\tCode    int      `json:\"code\"`\n\tMessage string   `json:\"message\"`\n\tData    FileInfo `json:\"data\"`\n}\n\ntype FileInfo struct {\n\tName        string      `json:\"name\"`\n\tNumBlocks   int         `json:\"num_blocks\"`\n\tVersion     string      `json:\"version\"`\n\tMimeType    string      `json:\"mime_type\"`\n\tMountPoint  string      `json:\"mount_point\"`\n\tPreviewMeta PreviewMeta `json:\"preview_meta\"`\n}\n\ntype PreviewMeta struct {\n\tData map[string]PreviewMetaEntry `json:\"data\"`\n}\n\ntype PreviewMetaEntry struct {\n\tStatus          int    `json:\"status\"`\n\tExtra           string `json:\"extra\"`\n\tPreviewFileSize int64  `json:\"preview_file_size\"`\n}\n\ntype PreviewImageExtra struct {\n\tImgExt   string `json:\"img_ext\"`\n\tPageNums int    `json:\"page_nums\"`\n}\n\ntype UserStorageResp struct {\n\tBaseResp\n\tData UserStorageData `json:\"data\"`\n}\n\ntype UserStorageData struct {\n\tShowSizeLimit       bool  `json:\"show_size_limit\"`\n\tTotalSizeLimitBytes int64 `json:\"total_size_limit_bytes\"`\n\tUsedSizeBytes       int64 `json:\"used_size_bytes\"`\n}\n\ntype UploadPrepareResp struct {\n\tBaseResp\n\tData UploadPrepareData `json:\"data\"`\n}\n\ntype UploadPrepareData struct {\n\tBlockSize       int64  `json:\"block_size\"`\n\tNumBlocks       int    `json:\"num_blocks\"`\n\tOptionBlockSize int64  `json:\"option_block_size\"`\n\tDedupeSupport   bool   `json:\"dedupe_support\"`\n\tUploadID        string `json:\"upload_id\"`\n}\n\ntype UploadBlock struct {\n\tHash       string `json:\"hash\"`\n\tSeq        int    `json:\"seq\"`\n\tSize       int64  `json:\"size\"`\n\tChecksum   string `json:\"checksum\"`\n\tIsUploaded bool   `json:\"isUploaded\"`\n}\n\ntype UploadBlocksResp struct {\n\tBaseResp\n\tData UploadBlocksData `json:\"data\"`\n}\n\ntype UploadBlocksData struct {\n\tNeededUploadBlocks []UploadBlockNeed `json:\"needed_upload_blocks\"`\n}\n\ntype UploadBlockNeed struct {\n\tSeq      int    `json:\"seq\"`\n\tSize     int64  `json:\"size\"`\n\tChecksum string `json:\"checksum\"`\n\tHash     string `json:\"hash\"`\n}\n\ntype UploadMergeResp struct {\n\tBaseResp\n\tData UploadMergeData `json:\"data\"`\n}\n\ntype UploadMergeData struct {\n\tSuccessSeqList []int `json:\"success_seq_list\"`\n}\n\ntype UploadFinishResp struct {\n\tBaseResp\n\tData UploadFinishData `json:\"data\"`\n}\n\ntype UploadFinishData struct {\n\tVersion     string `json:\"version\"`\n\tDataVersion string `json:\"data_version\"`\n\tExtra       struct {\n\t\tNodeToken string `json:\"node_token\"`\n\t} `json:\"extra\"`\n\tFileToken string `json:\"file_token\"`\n}\n\ntype RemoveResp struct {\n\tBaseResp\n\tData struct {\n\t\tTaskID string `json:\"task_id\"`\n\t} `json:\"data\"`\n}\n\ntype TaskStatusResp struct {\n\tBaseResp\n\tData TaskStatusData `json:\"data\"`\n}\n\ntype TaskStatusData struct {\n\tIsFinish bool `json:\"is_finish\"`\n\tIsFail   bool `json:\"is_fail\"`\n}\n"
  },
  {
    "path": "drivers/doubao_new/util.go",
    "content": "package doubao_new\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"hash/adler32\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tBaseURL         = \"https://my.feishu.cn\"\n\tDownloadBaseURL = \"https://internal-api-drive-stream.feishu.cn\"\n)\n\nvar defaultObjTypes = []string{\"124\", \"0\", \"12\", \"30\", \"123\", \"22\"}\n\nfunc (d *DoubaoNew) request(ctx context.Context, path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeader(\"accept\", \"*/*\")\n\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\treq.SetHeader(\"authorization\", auth)\n\t}\n\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\treq.SetHeader(\"dpop\", dpop)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tres, err := req.Execute(method, BaseURL+path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res != nil {\n\t\tif v := res.Header().Get(\"X-Tt-Logid\"); v != \"\" {\n\t\t\td.TtLogid = v\n\t\t} else if v := res.Header().Get(\"x-tt-logid\"); v != \"\" {\n\t\t\td.TtLogid = v\n\t\t}\n\t}\n\n\tbody := res.Body()\n\tvar common BaseResp\n\tif err = json.Unmarshal(body, &common); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn body, fmt.Errorf(msg)\n\t}\n\tif common.Code != 0 {\n\t\terrMsg := common.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = common.Message\n\t\t}\n\t\treturn body, fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", common.Code, errMsg)\n\t}\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 getCookieValue(cookie, name string) string {\n\tparts := strings.Split(cookie, \";\")\n\tprefix := name + \"=\"\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif strings.HasPrefix(part, prefix) {\n\t\t\treturn strings.TrimPrefix(part, prefix)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc adler32String(data []byte) string {\n\tsum := adler32.Checksum(data)\n\treturn strconv.FormatUint(uint64(sum), 10)\n}\n\nfunc buildCommaHeader(items []string) string {\n\treturn strings.Join(items, \",\")\n}\n\nfunc joinIntComma(items []int) string {\n\tif len(items) == 0 {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\tfor i, v := range items {\n\t\tif i > 0 {\n\t\t\tsb.WriteByte(',')\n\t\t}\n\t\tsb.WriteString(strconv.Itoa(v))\n\t}\n\treturn sb.String()\n}\n\nfunc previewList(items []string, n int) string {\n\tif n <= 0 || len(items) == 0 {\n\t\treturn \"\"\n\t}\n\tif len(items) < n {\n\t\tn = len(items)\n\t}\n\treturn strings.Join(items[:n], \",\")\n}\n\nfunc (d *DoubaoNew) resolveAuthorization() string {\n\tauth := strings.TrimSpace(d.Authorization)\n\tif auth == \"\" && d.Cookie != \"\" {\n\t\tif token := getCookieValue(d.Cookie, \"LARK_SUITE_ACCESS_TOKEN\"); token != \"\" {\n\t\t\tauth = token\n\t\t}\n\t}\n\tif auth == \"\" {\n\t\treturn \"\"\n\t}\n\tif !strings.HasPrefix(auth, \"DPoP \") && !strings.HasPrefix(auth, \"dpop \") {\n\t\tauth = \"DPoP \" + auth\n\t}\n\treturn auth\n}\n\nfunc (d *DoubaoNew) resolveDpop() string {\n\tdpop := strings.TrimSpace(d.Dpop)\n\tif dpop == \"\" && d.Cookie != \"\" {\n\t\tdpop = getCookieValue(d.Cookie, \"LARK_SUITE_DPOP\")\n\t}\n\treturn dpop\n}\n\nfunc (d *DoubaoNew) listChildren(ctx context.Context, parentToken string, lastLabel string) (ListData, error) {\n\tvar resp ListResp\n\t_, err := d.request(ctx, \"/space/api/explorer/doubao/children/list/\", http.MethodGet, func(req *resty.Request) {\n\t\tvalues := url.Values{}\n\t\tfor _, t := range defaultObjTypes {\n\t\t\tvalues.Add(\"obj_type\", t)\n\t\t}\n\t\tvalues.Set(\"length\", \"50\")\n\t\tvalues.Set(\"rank\", \"0\")\n\t\tvalues.Set(\"asc\", \"0\")\n\t\tvalues.Set(\"min_length\", \"40\")\n\t\tvalues.Set(\"thumbnail_width\", \"1028\")\n\t\tvalues.Set(\"thumbnail_height\", \"1028\")\n\t\tvalues.Set(\"thumbnail_policy\", \"4\")\n\t\tif parentToken != \"\" {\n\t\t\tvalues.Set(\"token\", parentToken)\n\t\t}\n\t\tif lastLabel != \"\" {\n\t\t\tvalues.Set(\"last_label\", lastLabel)\n\t\t}\n\t\treq.SetQueryParamsFromValues(values)\n\t}, &resp)\n\tif err != nil {\n\t\treturn ListData{}, err\n\t}\n\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) getFileInfo(ctx context.Context, fileToken string) (FileInfo, error) {\n\tvar resp FileInfoResp\n\t_, err := d.request(ctx, \"/space/api/box/file/info/\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetBody(base.Json{\n\t\t\t\"caller\":        \"explorer\",\n\t\t\t\"file_token\":    fileToken,\n\t\t\t\"mount_point\":   \"explorer\",\n\t\t\t\"option_params\": []string{\"preview_meta\", \"check_cipher\"},\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn FileInfo{}, err\n\t}\n\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) createFolder(ctx context.Context, parentToken, name string) (Node, error) {\n\tdata := url.Values{}\n\tdata.Set(\"name\", name)\n\tdata.Set(\"source\", \"0\")\n\tif parentToken != \"\" {\n\t\tdata.Set(\"parent_token\", parentToken)\n\t}\n\n\tdoRequest := func(csrfToken string) (*resty.Response, []byte, error) {\n\t\treq := base.RestyClient.R()\n\t\treq.SetContext(ctx)\n\t\treq.SetHeader(\"accept\", \"*/*\")\n\t\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\t\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\t\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\t\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\t\treq.SetHeader(\"authorization\", auth)\n\t\t}\n\t\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\t\treq.SetHeader(\"dpop\", dpop)\n\t\t}\n\t\tif csrfToken != \"\" {\n\t\t\treq.SetHeader(\"x-csrftoken\", csrfToken)\n\t\t}\n\t\treq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\treq.SetBody(data.Encode())\n\t\tres, err := req.Execute(http.MethodPost, BaseURL+\"/space/api/explorer/v2/create/folder/\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn res, res.Body(), nil\n\t}\n\n\tres, body, err := doRequestWithCsrf(doRequest)\n\tif err != nil {\n\t\treturn Node{}, err\n\t}\n\tif err := decodeBaseResp(body, res); err != nil {\n\t\treturn Node{}, err\n\t}\n\n\tvar resp CreateFolderResp\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn Node{}, fmt.Errorf(msg)\n\t}\n\n\tvar node Node\n\tif len(resp.Data.NodeList) > 0 {\n\t\tif n, ok := resp.Data.Entities.Nodes[resp.Data.NodeList[0]]; ok {\n\t\t\tnode = n\n\t\t}\n\t}\n\tif node.Token == \"\" {\n\t\tfor _, n := range resp.Data.Entities.Nodes {\n\t\t\tnode = n\n\t\t\tbreak\n\t\t}\n\t}\n\tif node.Token == \"\" && node.ObjToken == \"\" && node.NodeToken == \"\" {\n\t\treturn Node{}, fmt.Errorf(\"[doubao_new] create folder failed: empty response\")\n\t}\n\tif node.NodeToken == \"\" {\n\t\tif node.Token != \"\" {\n\t\t\tnode.NodeToken = node.Token\n\t\t} else if node.ObjToken != \"\" {\n\t\t\tnode.NodeToken = node.ObjToken\n\t\t}\n\t}\n\tif node.ObjToken == \"\" && node.Token != \"\" {\n\t\tnode.ObjToken = node.Token\n\t}\n\treturn node, nil\n}\n\nfunc (d *DoubaoNew) renameFolder(ctx context.Context, token, name string) error {\n\tif token == \"\" {\n\t\treturn fmt.Errorf(\"[doubao_new] rename folder missing token\")\n\t}\n\tdata := url.Values{}\n\tdata.Set(\"token\", token)\n\tdata.Set(\"name\", name)\n\n\tdoRequest := func(csrfToken string) (*resty.Response, []byte, error) {\n\t\treq := base.RestyClient.R()\n\t\treq.SetContext(ctx)\n\t\treq.SetHeader(\"accept\", \"*/*\")\n\t\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\t\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\t\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\t\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\t\treq.SetHeader(\"authorization\", auth)\n\t\t}\n\t\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\t\treq.SetHeader(\"dpop\", dpop)\n\t\t}\n\t\tif csrfToken != \"\" {\n\t\t\treq.SetHeader(\"x-csrftoken\", csrfToken)\n\t\t}\n\t\treq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\treq.SetBody(data.Encode())\n\t\tres, err := req.Execute(http.MethodPost, BaseURL+\"/space/api/explorer/v2/rename/\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn res, res.Body(), nil\n\t}\n\n\tres, body, err := doRequestWithCsrf(doRequest)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn decodeBaseResp(body, res)\n}\n\nfunc isCsrfTokenError(body []byte, res *resty.Response) bool {\n\tif len(body) == 0 {\n\t\treturn false\n\t}\n\tif strings.Contains(strings.ToLower(string(body)), \"csrf token error\") {\n\t\treturn true\n\t}\n\tif res != nil && res.StatusCode() == http.StatusForbidden {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc doRequestWithCsrf(doRequest func(csrfToken string) (*resty.Response, []byte, error)) (*resty.Response, []byte, error) {\n\tres, body, err := doRequest(\"\")\n\tif err != nil {\n\t\treturn res, body, err\n\t}\n\tif isCsrfTokenError(body, res) {\n\t\tcsrfToken := extractCsrfTokenFromResponse(res)\n\t\tif csrfToken != \"\" {\n\t\t\treturn doRequest(csrfToken)\n\t\t}\n\t}\n\treturn res, body, err\n}\n\nfunc extractCsrfTokenFromResponse(res *resty.Response) string {\n\tif res == nil || res.Request == nil {\n\t\treturn \"\"\n\t}\n\tif res.Request.RawRequest != nil {\n\t\tif csrf := getCookieValue(res.Request.RawRequest.Header.Get(\"Cookie\"), \"_csrf_token\"); csrf != \"\" {\n\t\t\treturn csrf\n\t\t}\n\t}\n\tif csrf := getCookieValue(res.Request.Header.Get(\"Cookie\"), \"_csrf_token\"); csrf != \"\" {\n\t\treturn csrf\n\t}\n\tfor _, c := range res.Cookies() {\n\t\tif c.Name == \"_csrf_token\" {\n\t\t\treturn c.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc decodeBaseResp(body []byte, res *resty.Response) error {\n\tvar common BaseResp\n\tif err := json.Unmarshal(body, &common); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn fmt.Errorf(msg)\n\t}\n\tif common.Code != 0 {\n\t\terrMsg := common.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = common.Message\n\t\t}\n\t\treturn fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", common.Code, errMsg)\n\t}\n\treturn nil\n}\n\nfunc (d *DoubaoNew) renameFile(ctx context.Context, fileToken, name string) error {\n\tif fileToken == \"\" {\n\t\treturn fmt.Errorf(\"[doubao_new] rename file missing file token\")\n\t}\n\t_, err := d.request(ctx, \"/space/api/box/file/update_info/\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetBody(base.Json{\n\t\t\t\"file_token\": fileToken,\n\t\t\t\"name\":       name,\n\t\t})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *DoubaoNew) moveObj(ctx context.Context, srcToken, destToken string) error {\n\tif srcToken == \"\" {\n\t\treturn fmt.Errorf(\"[doubao_new] move missing src token\")\n\t}\n\tdata := url.Values{}\n\tdata.Set(\"src_token\", srcToken)\n\tif destToken != \"\" {\n\t\tdata.Set(\"dest_token\", destToken)\n\t}\n\tdoRequest := func(csrfToken string) (*resty.Response, []byte, error) {\n\t\treq := base.RestyClient.R()\n\t\treq.SetContext(ctx)\n\t\treq.SetHeader(\"accept\", \"*/*\")\n\t\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\t\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\t\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\t\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\t\treq.SetHeader(\"authorization\", auth)\n\t\t}\n\t\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\t\treq.SetHeader(\"dpop\", dpop)\n\t\t}\n\t\tif csrfToken != \"\" {\n\t\t\treq.SetHeader(\"x-csrftoken\", csrfToken)\n\t\t}\n\t\treq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\treq.SetBody(data.Encode())\n\t\tres, err := req.Execute(http.MethodPost, BaseURL+\"/space/api/explorer/v2/move/\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn res, res.Body(), nil\n\t}\n\n\tres, body, err := doRequestWithCsrf(doRequest)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn decodeBaseResp(body, res)\n}\n\nfunc (d *DoubaoNew) removeObj(ctx context.Context, tokens []string) error {\n\tif len(tokens) == 0 {\n\t\treturn fmt.Errorf(\"[doubao_new] remove missing tokens\")\n\t}\n\tdoRequest := func(csrfToken string) (*resty.Response, []byte, error) {\n\t\treq := base.RestyClient.R()\n\t\treq.SetContext(ctx)\n\t\treq.SetHeader(\"accept\", \"application/json, text/plain, */*\")\n\t\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\t\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\t\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\t\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\t\treq.SetHeader(\"authorization\", auth)\n\t\t}\n\t\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\t\treq.SetHeader(\"dpop\", dpop)\n\t\t}\n\t\tif csrfToken != \"\" {\n\t\t\treq.SetHeader(\"x-csrftoken\", csrfToken)\n\t\t}\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetBody(base.Json{\n\t\t\t\"tokens\": tokens,\n\t\t\t\"apply\":  1,\n\t\t})\n\t\tres, err := req.Execute(http.MethodPost, BaseURL+\"/space/api/explorer/v3/remove/\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn res, res.Body(), nil\n\t}\n\n\tres, body, err := doRequestWithCsrf(doRequest)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar resp RemoveResp\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn fmt.Errorf(msg)\n\t}\n\tif resp.Code != 0 {\n\t\terrMsg := resp.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = resp.Message\n\t\t}\n\t\treturn fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", resp.Code, errMsg)\n\t}\n\tif resp.Data.TaskID == \"\" {\n\t\treturn nil\n\t}\n\treturn d.waitTask(ctx, resp.Data.TaskID)\n}\n\nfunc (d *DoubaoNew) getUserStorage(ctx context.Context) (UserStorageData, error) {\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeader(\"accept\", \"*/*\")\n\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\treq.SetHeader(\"agw-js-conv\", \"str\")\n\treq.SetHeader(\"content-type\", \"application/json\")\n\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\treq.SetHeader(\"authorization\", auth)\n\t}\n\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\treq.SetHeader(\"dpop\", dpop)\n\t}\n\tif d.Cookie != \"\" {\n\t\treq.SetHeader(\"cookie\", d.Cookie)\n\t}\n\treq.SetBody(base.Json{})\n\n\tres, err := req.Execute(http.MethodPost, \"https://www.doubao.com/alice/aispace/facade/get_user_storage\")\n\tif err != nil {\n\t\treturn UserStorageData{}, err\n\t}\n\n\tbody := res.Body()\n\tvar resp UserStorageResp\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn UserStorageData{}, fmt.Errorf(msg)\n\t}\n\tif resp.Code != 0 {\n\t\terrMsg := resp.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = resp.Message\n\t\t}\n\t\treturn UserStorageData{}, fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", resp.Code, errMsg)\n\t}\n\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) waitTask(ctx context.Context, taskID string) error {\n\tconst (\n\t\ttaskPollInterval    = time.Second\n\t\ttaskPollMaxAttempts = 120\n\t)\n\tvar lastErr error\n\tfor attempt := 0; attempt < taskPollMaxAttempts; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tif err := waitWithContext(ctx, taskPollInterval); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tstatus, err := d.getTaskStatus(ctx, taskID)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\t\tif status.IsFail {\n\t\t\treturn fmt.Errorf(\"[doubao_new] remove task failed: %s\", taskID)\n\t\t}\n\t\tif status.IsFinish {\n\t\t\treturn nil\n\t\t}\n\t}\n\tif lastErr != nil {\n\t\treturn lastErr\n\t}\n\treturn fmt.Errorf(\"[doubao_new] remove task timed out: %s\", taskID)\n}\n\nfunc (d *DoubaoNew) getTaskStatus(ctx context.Context, taskID string) (TaskStatusData, error) {\n\tif taskID == \"\" {\n\t\treturn TaskStatusData{}, fmt.Errorf(\"[doubao_new] task status missing task_id\")\n\t}\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeader(\"accept\", \"application/json, text/plain, */*\")\n\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\treq.SetHeader(\"authorization\", auth)\n\t}\n\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\treq.SetHeader(\"dpop\", dpop)\n\t}\n\treq.SetQueryParam(\"task_id\", taskID)\n\tres, err := req.Execute(http.MethodGet, BaseURL+\"/space/api/explorer/v2/task/\")\n\tif err != nil {\n\t\treturn TaskStatusData{}, err\n\t}\n\tbody := res.Body()\n\tvar resp TaskStatusResp\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn TaskStatusData{}, fmt.Errorf(msg)\n\t}\n\tif resp.Code != 0 {\n\t\terrMsg := resp.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = resp.Message\n\t\t}\n\t\treturn TaskStatusData{}, fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", resp.Code, errMsg)\n\t}\n\treturn resp.Data, nil\n}\n\nfunc waitWithContext(ctx context.Context, d time.Duration) error {\n\ttimer := time.NewTimer(d)\n\tdefer timer.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n\nfunc (d *DoubaoNew) prepareUpload(ctx context.Context, name string, size int64, mountNodeToken string) (UploadPrepareData, error) {\n\tvar resp UploadPrepareResp\n\t_, err := d.request(ctx, \"/space/api/box/upload/prepare/\", http.MethodPost, func(req *resty.Request) {\n\t\tvalues := url.Values{}\n\t\tvalues.Set(\"shouldBypassScsDialog\", \"true\")\n\t\tvalues.Set(\"doubao_storage\", \"imagex_other\")\n\t\tvalues.Set(\"doubao_app_id\", \"497858\")\n\t\treq.SetQueryParamsFromValues(values)\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetHeader(\"x-command\", \"space.api.box.upload.prepare\")\n\t\treq.SetHeader(\"rpc-persist-doubao-pan\", \"true\")\n\t\treq.SetHeader(\"cache-control\", \"no-cache\")\n\t\treq.SetHeader(\"pragma\", \"no-cache\")\n\t\tbody := base.Json{\n\t\t\t\"mount_point\":      \"explorer\",\n\t\t\t\"mount_node_token\": \"\",\n\t\t\t\"name\":             name,\n\t\t\t\"size\":             size,\n\t\t\t\"size_checker\":     true,\n\t\t}\n\t\tif mountNodeToken != \"\" {\n\t\t\tbody[\"mount_node_token\"] = mountNodeToken\n\t\t}\n\t\treq.SetBody(body)\n\t}, &resp)\n\tif err != nil {\n\t\treturn UploadPrepareData{}, err\n\t}\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) uploadBlocks(ctx context.Context, uploadID string, blocks []UploadBlock, mountPoint string) (UploadBlocksData, error) {\n\tif uploadID == \"\" {\n\t\treturn UploadBlocksData{}, fmt.Errorf(\"[doubao_new] upload blocks missing upload_id\")\n\t}\n\tif mountPoint == \"\" {\n\t\tmountPoint = \"explorer\"\n\t}\n\tvar resp UploadBlocksResp\n\t_, err := d.request(ctx, \"/space/api/box/upload/blocks/\", http.MethodPost, func(req *resty.Request) {\n\t\tvalues := url.Values{}\n\t\tvalues.Set(\"shouldBypassScsDialog\", \"true\")\n\t\tvalues.Set(\"doubao_storage\", \"imagex_other\")\n\t\tvalues.Set(\"doubao_app_id\", \"497858\")\n\t\treq.SetQueryParamsFromValues(values)\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetHeader(\"x-command\", \"space.api.box.upload.blocks\")\n\t\treq.SetHeader(\"rpc-persist-doubao-pan\", \"true\")\n\t\treq.SetHeader(\"cache-control\", \"no-cache\")\n\t\treq.SetHeader(\"pragma\", \"no-cache\")\n\t\treq.SetBody(base.Json{\n\t\t\t\"blocks\":      blocks,\n\t\t\t\"upload_id\":   uploadID,\n\t\t\t\"mount_point\": mountPoint,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn UploadBlocksData{}, err\n\t}\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) mergeUploadBlocks(ctx context.Context, uploadID string, seqList []int, checksumList []string, sizeList []int64, blockOriginSize int64, data []byte) (UploadMergeData, error) {\n\tif uploadID == \"\" {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks missing upload_id\")\n\t}\n\tif len(seqList) == 0 {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks empty seq list\")\n\t}\n\tif len(checksumList) == 0 {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks empty checksum list\")\n\t}\n\tif len(sizeList) != len(seqList) {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks size list mismatch\")\n\t}\n\tif blockOriginSize <= 0 {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks invalid block origin size\")\n\t}\n\tif len(data) == 0 {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks empty data\")\n\t}\n\n\tseqHeader := joinIntComma(seqList)\n\tchecksumHeader := buildCommaHeader(checksumList)\n\n\tclient := base.NewRestyClient()\n\tclient.SetCookieJar(nil)\n\treq := client.R()\n\treq.SetContext(ctx)\n\treq.SetHeader(\"accept\", \"application/json, text/plain, */*\")\n\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\treq.SetHeader(\"rpc-persist-doubao-pan\", \"true\")\n\treq.SetHeader(\"content-type\", \"application/octet-stream\")\n\treq.Header.Set(\"x-block-list-checksum\", checksumHeader)\n\treq.Header.Set(\"x-seq-list\", seqHeader)\n\treq.SetHeader(\"x-block-origin-size\", strconv.FormatInt(blockOriginSize, 10))\n\treq.SetHeader(\"x-command\", \"space.api.box.stream.upload.merge_block\")\n\treq.SetHeader(\"x-csrftoken\", \"\")\n\treqID := \"\"\n\tif buf := make([]byte, 16); true {\n\t\tif _, err := rand.Read(buf); err == nil {\n\t\t\treqID = hex.EncodeToString(buf)\n\t\t}\n\t}\n\tif reqID != \"\" {\n\t\treq.SetHeader(\"x-request-id\", reqID)\n\t}\n\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\treq.SetHeader(\"authorization\", auth)\n\t}\n\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\treq.SetHeader(\"dpop\", dpop)\n\t}\n\treq.Header.Del(\"Cookie\")\n\treq.Header.Del(\"cookie\")\n\tif req.Header.Get(\"x-command\") == \"\" {\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] merge blocks missing x-command header\")\n\t}\n\treq.SetBody(data)\n\n\tvalues := url.Values{}\n\tvalues.Set(\"shouldBypassScsDialog\", \"true\")\n\tvalues.Set(\"upload_id\", uploadID)\n\tvalues.Set(\"mount_point\", \"explorer\")\n\tvalues.Set(\"doubao_storage\", \"imagex_other\")\n\tvalues.Set(\"doubao_app_id\", \"497858\")\n\turlStr := \"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?\" + values.Encode()\n\n\tres, err := req.Execute(http.MethodPost, urlStr)\n\tif err != nil {\n\t\treturn UploadMergeData{}, err\n\t}\n\tif v := res.Header().Get(\"X-Tt-Logid\"); v != \"\" {\n\t\td.TtLogid = v\n\t} else if v := res.Header().Get(\"x-tt-logid\"); v != \"\" {\n\t\td.TtLogid = v\n\t}\n\tbody := res.Body()\n\tvar resp UploadMergeResp\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\tmsg := fmt.Sprintf(\"[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v\",\n\t\t\tres.Status(),\n\t\t\tres.Header().Get(\"Content-Type\"),\n\t\t\tstring(body),\n\t\t\terr,\n\t\t)\n\t\treturn UploadMergeData{}, fmt.Errorf(msg)\n\t}\n\tif resp.Code != 0 {\n\t\tif res != nil && res.StatusCode() == http.StatusBadRequest && resp.Code == 2 {\n\t\t\tsuccess := make([]int, 0, len(seqList))\n\t\t\toffset := 0\n\t\t\tfor i, seq := range seqList {\n\t\t\t\tsize := sizeList[i]\n\t\t\t\tif size <= 0 {\n\t\t\t\t\treturn UploadMergeData{SuccessSeqList: success}, fmt.Errorf(\"[doubao_new] v3 fallback invalid size: seq=%d size=%d\", seq, size)\n\t\t\t\t}\n\t\t\t\tif offset+int(size) > len(data) {\n\t\t\t\t\treturn UploadMergeData{SuccessSeqList: success}, fmt.Errorf(\"[doubao_new] v3 fallback payload out of range: seq=%d offset=%d size=%d total=%d\", seq, offset, size, len(data))\n\t\t\t\t}\n\t\t\t\tpayload := data[offset : offset+int(size)]\n\t\t\t\tblock := UploadBlockNeed{\n\t\t\t\t\tSeq:      seq,\n\t\t\t\t\tSize:     size,\n\t\t\t\t\tChecksum: checksumList[i],\n\t\t\t\t}\n\t\t\t\tif err := d.uploadBlockV3(ctx, uploadID, block, payload); err != nil {\n\t\t\t\t\treturn UploadMergeData{SuccessSeqList: success}, err\n\t\t\t\t}\n\t\t\t\tsuccess = append(success, seq)\n\t\t\t\toffset += int(size)\n\t\t\t}\n\t\t\treturn UploadMergeData{SuccessSeqList: success}, nil\n\t\t}\n\t\terrMsg := resp.Msg\n\t\tif errMsg == \"\" {\n\t\t\terrMsg = resp.Message\n\t\t}\n\t\treturn UploadMergeData{}, fmt.Errorf(\"[doubao_new] API error (code: %d): %s\", resp.Code, errMsg)\n\t}\n\n\treturn resp.Data, nil\n}\n\nfunc (d *DoubaoNew) uploadBlockV3(ctx context.Context, uploadID string, block UploadBlockNeed, data []byte) error {\n\tif uploadID == \"\" {\n\t\treturn fmt.Errorf(\"[doubao_new] upload v3 block missing upload_id\")\n\t}\n\tif block.Seq < 0 {\n\t\treturn fmt.Errorf(\"[doubao_new] upload v3 block invalid seq\")\n\t}\n\tif len(data) == 0 {\n\t\treturn fmt.Errorf(\"[doubao_new] upload v3 block empty data\")\n\t}\n\n\treq := base.RestyClient.R()\n\treq.SetContext(ctx)\n\treq.SetHeader(\"accept\", \"*/*\")\n\treq.SetHeader(\"origin\", \"https://www.doubao.com\")\n\treq.SetHeader(\"referer\", \"https://www.doubao.com/\")\n\treq.SetHeader(\"user-agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\")\n\treq.SetHeader(\"rpc-persist-doubao-pan\", \"true\")\n\treq.SetHeader(\"x-block-seq\", strconv.Itoa(block.Seq))\n\treq.SetHeader(\"x-block-checksum\", block.Checksum)\n\tif auth := d.resolveAuthorization(); auth != \"\" {\n\t\treq.SetHeader(\"authorization\", auth)\n\t}\n\tif dpop := d.resolveDpop(); dpop != \"\" {\n\t\treq.SetHeader(\"dpop\", dpop)\n\t}\n\n\treq.SetMultipartFormData(map[string]string{\n\t\t\"upload_id\": uploadID,\n\t\t\"size\":      strconv.FormatInt(int64(len(data)), 10),\n\t})\n\treq.SetMultipartField(\"file\", \"blob\", \"application/octet-stream\", bytes.NewReader(data))\n\n\tvalues := url.Values{}\n\tvalues.Set(\"shouldBypassScsDialog\", \"true\")\n\tvalues.Set(\"upload_id\", uploadID)\n\tvalues.Set(\"seq\", strconv.Itoa(block.Seq))\n\tvalues.Set(\"size\", strconv.FormatInt(int64(len(data)), 10))\n\tvalues.Set(\"checksum\", block.Checksum)\n\tvalues.Set(\"mount_point\", \"explorer\")\n\tvalues.Set(\"doubao_storage\", \"imagex_other\")\n\tvalues.Set(\"doubao_app_id\", \"497858\")\n\turlStr := \"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/v3/block/?\" + values.Encode()\n\n\tres, err := req.Execute(http.MethodPost, urlStr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbody := res.Body()\n\tif err := decodeBaseResp(body, res); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DoubaoNew) finishUpload(ctx context.Context, uploadID string, numBlocks int, mountPoint string) (UploadFinishData, error) {\n\tif uploadID == \"\" {\n\t\treturn UploadFinishData{}, fmt.Errorf(\"[doubao_new] finish upload missing upload_id\")\n\t}\n\tif numBlocks <= 0 {\n\t\treturn UploadFinishData{}, fmt.Errorf(\"[doubao_new] finish upload invalid num_blocks\")\n\t}\n\tif mountPoint == \"\" {\n\t\tmountPoint = \"explorer\"\n\t}\n\tvar resp UploadFinishResp\n\t_, err := d.request(ctx, \"/space/api/box/upload/finish/\", http.MethodPost, func(req *resty.Request) {\n\t\tvalues := url.Values{}\n\t\tvalues.Set(\"shouldBypassScsDialog\", \"true\")\n\t\tvalues.Set(\"doubao_storage\", \"imagex_other\")\n\t\tvalues.Set(\"doubao_app_id\", \"497858\")\n\t\treq.SetQueryParamsFromValues(values)\n\t\treq.SetHeader(\"Content-Type\", \"application/json\")\n\t\treq.SetHeader(\"x-command\", \"space.api.box.upload.finish\")\n\t\treq.SetHeader(\"rpc-persist-doubao-pan\", \"true\")\n\t\treq.SetHeader(\"cache-control\", \"no-cache\")\n\t\treq.SetHeader(\"pragma\", \"no-cache\")\n\t\treq.SetHeader(\"biz-scene\", \"file_upload\")\n\t\treq.SetHeader(\"biz-ua-type\", \"Web\")\n\t\treq.SetBody(base.Json{\n\t\t\t\"upload_id\":                uploadID,\n\t\t\t\"num_blocks\":               numBlocks,\n\t\t\t\"mount_point\":              mountPoint,\n\t\t\t\"push_open_history_record\": 1,\n\t\t})\n\t}, &resp)\n\tif err != nil {\n\t\treturn UploadFinishData{}, err\n\t}\n\treturn resp.Data, nil\n}\n"
  },
  {
    "path": "drivers/doubao_share/driver.go",
    "content": "package doubao_share\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"net/http\"\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\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 GetFileUrlResp\n\t\t\t_, err := d.request(\"/alice/message/get_file_url\", http.MethodPost, func(req *resty.Request) {\n\t\t\t\treq.SetBody(base.Json{\n\t\t\t\t\t\"uris\": []string{u.Key},\n\t\t\t\t\t\"type\": FileNodeType[u.NodeType],\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.FileUrls[0].MainURL\n\t\t}\n\n\t\t// 生成标准的Content-Disposition\n\t\tcontentDisposition := 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          true,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\t\"github.com/alist-org/alist/v3/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          int    `json:\"create_time\"`\n\t\tUpdateTime          int    `json:\"update_time\"`\n\t} `json:\"first_node\"`\n\tNodeCount      int    `json:\"node_count\"`\n\tCreateTime     int    `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          int    `json:\"create_time\"`\n\tUpdateTime          int    `json:\"update_time\"`\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 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\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\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     = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36\"\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\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": "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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.NewRequest(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 = req.WithContext(ctx)\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\nconst (\n\tDefaultClientID = \"76lrwrklhdn1icb\"\n)\n\ntype Addition struct {\n\tRefreshToken string `json:\"refresh_token\" required:\"true\"`\n\tdriver.RootPath\n\n\tOauthTokenURL string `json:\"oauth_token_url\" default:\"https://api.xhofe.top/alist/dropbox/token\"`\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\n\tAccessToken     string\n\tRootNamespaceId string\n}\n\nvar config = driver.Config{\n\tName:              \"Dropbox\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"\",\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"time\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (d *Dropbox) refreshToken() error {\n\turl := d.base + \"/oauth2/token\"\n\tif utils.SliceContains([]string{\"\", DefaultClientID}, d.ClientID) {\n\t\turl = d.OauthTokenURL\n\t}\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.NewRequest(http.MethodPost, url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq = req.WithContext(ctx)\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.AccessToken)\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.NewRequest(http.MethodPost, url, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq = req.WithContext(ctx)\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.AccessToken)\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"
  },
  {
    "path": "drivers/febbox/driver.go",
    "content": "package febbox\n\nimport (\n\t\"context\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          true,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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.NewRequest(\"POST\", 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.WithContext(c.ctx))\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/pkg/utils/hash\"\n\t\"strconv\"\n\t\"time\"\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\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"net/http\"\n\t\"strconv\"\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\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/jlaffaye/ftp\"\n)\n\ntype FTP struct {\n\tmodel.Storage\n\tAddition\n\tconn *ftp.ServerConn\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\treturn d.login()\n}\n\nfunc (d *FTP) Drop(ctx context.Context) error {\n\tif d.conn != nil {\n\t\t_ = d.conn.Logout()\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\tf := model.Object{\n\t\t\tName:     decode(entry.Name, d.Encoding),\n\t\t\tSize:     int64(entry.Size),\n\t\t\tModified: entry.Time,\n\t\t\tIsFolder: entry.Type == ftp.EntryTypeFolder,\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\tif err := d.login(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tr := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize())\n\tlink := &model.Link{\n\t\tMFile: r,\n\t}\n\treturn link, 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:   true,\n\tDefaultRoot: \"/\",\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\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/jlaffaye/ftp\"\n)\n\n// do others that not defined in Driver interface\n\nfunc (d *FTP) login() error {\n\tif d.conn != nil {\n\t\t_, err := d.conn.CurrentDir()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\tconn, err := ftp.Dial(d.Address, ftp.DialWithShutTimeout(10*time.Second))\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = conn.Login(d.Username, d.Password)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.conn = conn\n\treturn nil\n}\n\n// FileReader An FTP file reader that implements io.MFile for seeking.\ntype FileReader struct {\n\tconn         *ftp.ServerConn\n\tresp         *ftp.Response\n\toffset       atomic.Int64\n\treadAtOffset int64\n\tmu           sync.Mutex\n\tpath         string\n\tsize         int64\n}\n\nfunc NewFileReader(conn *ftp.ServerConn, path string, size int64) *FileReader {\n\treturn &FileReader{\n\t\tconn: conn,\n\t\tpath: path,\n\t\tsize: size,\n\t}\n}\n\nfunc (r *FileReader) Read(buf []byte) (n int, err error) {\n\tn, err = r.ReadAt(buf, r.offset.Load())\n\tr.offset.Add(int64(n))\n\treturn\n}\n\nfunc (r *FileReader) ReadAt(buf []byte, off int64) (n int, err error) {\n\tif off < 0 {\n\t\treturn -1, os.ErrInvalid\n\t}\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tif off != r.readAtOffset {\n\t\t//have to restart the connection, to correct offset\n\t\t_ = r.resp.Close()\n\t\tr.resp = nil\n\t}\n\n\tif r.resp == nil {\n\t\tr.resp, err = r.conn.RetrFrom(r.path, uint64(off))\n\t\tr.readAtOffset = off\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\tn, err = r.resp.Read(buf)\n\tr.readAtOffset += int64(n)\n\treturn\n}\n\nfunc (r *FileReader) Seek(offset int64, whence int) (int64, error) {\n\toldOffset := r.offset.Load()\n\tvar newOffset int64\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tnewOffset = offset\n\tcase io.SeekCurrent:\n\t\tnewOffset = oldOffset + offset\n\tcase io.SeekEnd:\n\t\treturn r.size, nil\n\tdefault:\n\t\treturn -1, os.ErrInvalid\n\t}\n\n\tif newOffset < 0 {\n\t\t// offset out of range\n\t\treturn oldOffset, os.ErrInvalid\n\t}\n\tif newOffset == oldOffset {\n\t\t// offset not changed, so return directly\n\t\treturn oldOffset, nil\n\t}\n\tr.offset.Store(newOffset)\n\treturn newOffset, nil\n}\n\nfunc (r *FileReader) Close() error {\n\tif r.resp != nil {\n\t\treturn r.resp.Close()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/ftps/driver.go",
    "content": "package ftps\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/jlaffaye/ftp\"\n)\n\ntype FTPS struct {\n\tmodel.Storage\n\tAddition\n\tconn *ftp.ServerConn\n}\n\nfunc (d *FTPS) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *FTPS) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *FTPS) Init(ctx context.Context) error {\n\treturn d.login()\n}\n\nfunc (d *FTPS) Drop(ctx context.Context) error {\n\tif d.conn != nil {\n\t\t_ = d.conn.Logout()\n\t}\n\treturn nil\n}\n\nfunc (d *FTPS) 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\tf := model.Object{\n\t\t\tName:     decode(entry.Name, d.Encoding),\n\t\t\tSize:     int64(entry.Size),\n\t\t\tModified: entry.Time,\n\t\t\tIsFolder: entry.Type == ftp.EntryTypeFolder,\n\t\t}\n\t\tres = append(res, &f)\n\t}\n\treturn res, nil\n}\n\nfunc (d *FTPS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif err := d.login(); err != nil {\n\t\treturn nil, err\n\t}\n\tr := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize())\n\tlink := &model.Link{\n\t\tMFile: r,\n\t}\n\treturn link, nil\n}\n\nfunc (d *FTPS) 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 *FTPS) 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 *FTPS) 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 *FTPS) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotSupport\n}\n\nfunc (d *FTPS) 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 *FTPS) 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 = (*FTPS)(nil)\n"
  },
  {
    "path": "drivers/ftps/meta.go",
    "content": "package ftps\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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:\"false\"`\n\tUsername              string `json:\"username\" required:\"true\"`\n\tPassword              string `json:\"password\" required:\"true\"`\n\tTLSMode               string `json:\"tls_mode\" type:\"select\" options:\"Explicit,Implicit\" default:\"Explicit\" required:\"true\" help:\"Explicit: STARTTLS on port 21; Implicit: direct TLS on port 990\"`\n\tTLSInsecureSkipVerify bool   `json:\"tls_insecure_skip_verify\" default:\"false\" help:\"Allow insecure TLS connections (e.g. self-signed certificates)\"`\n\tdriver.RootPath\n}\n\nvar config = driver.Config{\n\tName:        \"FTPS\",\n\tLocalSort:   true,\n\tOnlyLocal:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &FTPS{}\n\t})\n}\n"
  },
  {
    "path": "drivers/ftps/types.go",
    "content": "package ftps\n"
  },
  {
    "path": "drivers/ftps/util.go",
    "content": "package ftps\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/jlaffaye/ftp\"\n)\n\nfunc (d *FTPS) login() error {\n\tif d.conn != nil {\n\t\t_, err := d.conn.CurrentDir()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\thost, _, err := net.SplitHostPort(d.Address)\n\tif err != nil {\n\t\thost = d.Address\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tServerName:         host,\n\t\tInsecureSkipVerify: d.TLSInsecureSkipVerify,\n\t}\n\n\topts := []ftp.DialOption{\n\t\tftp.DialWithShutTimeout(10 * time.Second),\n\t}\n\tif d.TLSMode == \"Implicit\" {\n\t\topts = append(opts, ftp.DialWithTLS(tlsConfig))\n\t} else {\n\t\topts = append(opts, ftp.DialWithExplicitTLS(tlsConfig))\n\t}\n\n\tconn, err := ftp.Dial(d.Address, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = conn.Login(d.Username, d.Password)\n\tif err != nil {\n\t\t_ = conn.Quit()\n\t\treturn err\n\t}\n\td.conn = conn\n\treturn nil\n}\n\ntype FileReader struct {\n\tconn         *ftp.ServerConn\n\tresp         *ftp.Response\n\toffset       atomic.Int64\n\treadAtOffset int64\n\tmu           sync.Mutex\n\tpath         string\n\tsize         int64\n}\n\nfunc NewFileReader(conn *ftp.ServerConn, path string, size int64) *FileReader {\n\treturn &FileReader{\n\t\tconn: conn,\n\t\tpath: path,\n\t\tsize: size,\n\t}\n}\n\nfunc (r *FileReader) Read(buf []byte) (n int, err error) {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\toff := r.offset.Load()\n\tn, err = r.readAtLocked(buf, off)\n\tr.offset.Add(int64(n))\n\treturn\n}\n\nfunc (r *FileReader) ReadAt(buf []byte, off int64) (n int, err error) {\n\tif off < 0 {\n\t\treturn 0, os.ErrInvalid\n\t}\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\treturn r.readAtLocked(buf, off)\n}\n\nfunc (r *FileReader) readAtLocked(buf []byte, off int64) (n int, err error) {\n\tif r.resp != nil && off != r.readAtOffset {\n\t\t_ = r.resp.Close()\n\t\tr.resp = nil\n\t}\n\n\tif r.resp == nil {\n\t\tr.resp, err = r.conn.RetrFrom(r.path, uint64(off))\n\t\tr.readAtOffset = off\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\tn, err = r.resp.Read(buf)\n\tr.readAtOffset += int64(n)\n\treturn\n}\n\nfunc (r *FileReader) Seek(offset int64, whence int) (int64, error) {\n\toldOffset := r.offset.Load()\n\tvar newOffset int64\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tnewOffset = offset\n\tcase io.SeekCurrent:\n\t\tnewOffset = oldOffset + offset\n\tcase io.SeekEnd:\n\t\tnewOffset = r.size + offset\n\tdefault:\n\t\treturn -1, os.ErrInvalid\n\t}\n\n\tif newOffset < 0 {\n\t\treturn oldOffset, os.ErrInvalid\n\t}\n\tif newOffset == oldOffset {\n\t\treturn oldOffset, nil\n\t}\n\tr.offset.Store(newOffset)\n\treturn newOffset, nil\n}\n\nfunc (r *FileReader) Close() error {\n\tif r.resp != nil {\n\t\treturn r.resp.Close()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/gitee/driver.go",
    "content": "package gitee\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Gitee struct {\n\tmodel.Storage\n\tAddition\n\tclient *resty.Client\n}\n\nfunc (d *Gitee) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Gitee) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Gitee) Init(ctx context.Context) error {\n\td.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)\n\td.Endpoint = strings.TrimSpace(d.Endpoint)\n\tif d.Endpoint == \"\" {\n\t\td.Endpoint = \"https://gitee.com/api/v5\"\n\t}\n\td.Endpoint = strings.TrimSuffix(d.Endpoint, \"/\")\n\td.Owner = strings.TrimSpace(d.Owner)\n\td.Repo = strings.TrimSpace(d.Repo)\n\td.Token = strings.TrimSpace(d.Token)\n\td.DownloadProxy = strings.TrimSpace(d.DownloadProxy)\n\tif d.Owner == \"\" || d.Repo == \"\" {\n\t\treturn errors.New(\"owner and repo are required\")\n\t}\n\td.client = base.NewRestyClient().\n\t\tSetBaseURL(d.Endpoint).\n\t\tSetHeader(\"Accept\", \"application/json\")\n\trepo, err := d.getRepo()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.Ref = strings.TrimSpace(d.Ref)\n\tif d.Ref == \"\" {\n\t\td.Ref = repo.DefaultBranch\n\t}\n\treturn nil\n}\n\nfunc (d *Gitee) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Gitee) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\trelPath := d.relativePath(dir.GetPath())\n\tcontents, err := d.listContents(relPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobjs := make([]model.Obj, 0, len(contents))\n\tfor i := range contents {\n\t\tobjs = append(objs, contents[i].toModelObj())\n\t}\n\treturn objs, nil\n}\n\nfunc (d *Gitee) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tvar downloadURL string\n\tif obj, ok := file.(*Object); ok {\n\t\tdownloadURL = obj.DownloadURL\n\t\tif downloadURL == \"\" {\n\t\t\trelPath := d.relativePath(file.GetPath())\n\t\t\tcontent, err := d.getContent(relPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif content.DownloadURL == \"\" {\n\t\t\t\treturn nil, errors.New(\"empty download url\")\n\t\t\t}\n\t\t\tobj.DownloadURL = content.DownloadURL\n\t\t\tdownloadURL = content.DownloadURL\n\t\t}\n\t} else {\n\t\trelPath := d.relativePath(file.GetPath())\n\t\tcontent, err := d.getContent(relPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif content.DownloadURL == \"\" {\n\t\t\treturn nil, errors.New(\"empty download url\")\n\t\t}\n\t\tdownloadURL = content.DownloadURL\n\t}\n\turl := d.applyProxy(downloadURL)\n\treturn &model.Link{\n\t\tURL: url,\n\t\tHeader: http.Header{\n\t\t\t\"Cookie\": {d.Cookie},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Gitee) newRequest() *resty.Request {\n\treq := d.client.R()\n\tif d.Token != \"\" {\n\t\treq.SetQueryParam(\"access_token\", d.Token)\n\t}\n\tif d.Ref != \"\" {\n\t\treq.SetQueryParam(\"ref\", d.Ref)\n\t}\n\treturn req\n}\n\nfunc (d *Gitee) apiPath(path string) string {\n\tescapedOwner := url.PathEscape(d.Owner)\n\tescapedRepo := url.PathEscape(d.Repo)\n\tif path == \"\" {\n\t\treturn fmt.Sprintf(\"/repos/%s/%s/contents\", escapedOwner, escapedRepo)\n\t}\n\treturn fmt.Sprintf(\"/repos/%s/%s/contents/%s\", escapedOwner, escapedRepo, encodePath(path))\n}\n\nfunc (d *Gitee) listContents(path string) ([]Content, error) {\n\tres, err := d.newRequest().Get(d.apiPath(path))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.IsError() {\n\t\treturn nil, toErr(res)\n\t}\n\tvar contents []Content\n\tif err := utils.Json.Unmarshal(res.Body(), &contents); err != nil {\n\t\tvar single Content\n\t\tif err2 := utils.Json.Unmarshal(res.Body(), &single); err2 == nil && single.Type != \"\" {\n\t\t\tif single.Type != \"dir\" {\n\t\t\t\treturn nil, errs.NotFolder\n\t\t\t}\n\t\t\treturn []Content{}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tfor i := range contents {\n\t\tcontents[i].Path = joinPath(path, contents[i].Name)\n\t}\n\treturn contents, nil\n}\n\nfunc (d *Gitee) getContent(path string) (*Content, error) {\n\tres, err := d.newRequest().Get(d.apiPath(path))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.IsError() {\n\t\treturn nil, toErr(res)\n\t}\n\tvar content Content\n\tif err := utils.Json.Unmarshal(res.Body(), &content); err != nil {\n\t\treturn nil, err\n\t}\n\tif content.Type == \"\" {\n\t\treturn nil, errors.New(\"invalid response\")\n\t}\n\tif content.Path == \"\" {\n\t\tcontent.Path = path\n\t}\n\treturn &content, nil\n}\n\nfunc (d *Gitee) relativePath(full string) string {\n\tfull = utils.FixAndCleanPath(full)\n\troot := utils.FixAndCleanPath(d.RootFolderPath)\n\tif root == \"/\" {\n\t\treturn strings.TrimPrefix(full, \"/\")\n\t}\n\tif utils.PathEqual(full, root) {\n\t\treturn \"\"\n\t}\n\tprefix := utils.PathAddSeparatorSuffix(root)\n\tif strings.HasPrefix(full, prefix) {\n\t\treturn strings.TrimPrefix(full, prefix)\n\t}\n\treturn strings.TrimPrefix(full, \"/\")\n}\n\nfunc (d *Gitee) applyProxy(raw string) string {\n\tif raw == \"\" || d.DownloadProxy == \"\" {\n\t\treturn raw\n\t}\n\tproxy := d.DownloadProxy\n\tif !strings.HasSuffix(proxy, \"/\") {\n\t\tproxy += \"/\"\n\t}\n\treturn proxy + strings.TrimLeft(raw, \"/\")\n}\n\nfunc encodePath(p string) string {\n\tif p == \"\" {\n\t\treturn \"\"\n\t}\n\tparts := strings.Split(p, \"/\")\n\tfor i, part := range parts {\n\t\tparts[i] = url.PathEscape(part)\n\t}\n\treturn strings.Join(parts, \"/\")\n}\n\nfunc joinPath(base, name string) string {\n\tif base == \"\" {\n\t\treturn name\n\t}\n\treturn strings.TrimPrefix(stdpath.Join(base, name), \"./\")\n}\n"
  },
  {
    "path": "drivers/gitee/meta.go",
    "content": "package gitee\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tEndpoint      string `json:\"endpoint\" type:\"string\" help:\"Gitee API endpoint, default https://gitee.com/api/v5\"`\n\tToken         string `json:\"token\" type:\"string\"`\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:\"Branch, tag or commit SHA, defaults to repository default branch\"`\n\tDownloadProxy string `json:\"download_proxy\" type:\"string\" help:\"Prefix added before download URLs, e.g. https://mirror.example.com/\"`\n\tCookie        string `json:\"cookie\" type:\"string\" help:\"Cookie returned from user info request\"`\n}\n\nvar config = driver.Config{\n\tName:        \"Gitee\",\n\tLocalSort:   true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Gitee{}\n\t})\n}\n"
  },
  {
    "path": "drivers/gitee/types.go",
    "content": "package gitee\n\nimport (\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\ntype Links struct {\n\tSelf string `json:\"self\"`\n\tHtml string `json:\"html\"`\n}\n\ntype Content struct {\n\tType        string `json:\"type\"`\n\tSize        *int64 `json:\"size\"`\n\tName        string `json:\"name\"`\n\tPath        string `json:\"path\"`\n\tSha         string `json:\"sha\"`\n\tURL         string `json:\"url\"`\n\tHtmlURL     string `json:\"html_url\"`\n\tDownloadURL string `json:\"download_url\"`\n\tLinks       Links  `json:\"_links\"`\n}\n\nfunc (c Content) toModelObj() model.Obj {\n\tsize := int64(0)\n\tif c.Size != nil {\n\t\tsize = *c.Size\n\t}\n\treturn &Object{\n\t\tObject: model.Object{\n\t\t\tID:       c.Path,\n\t\t\tName:     c.Name,\n\t\t\tSize:     size,\n\t\t\tModified: time.Unix(0, 0),\n\t\t\tIsFolder: c.Type == \"dir\",\n\t\t},\n\t\tDownloadURL: c.DownloadURL,\n\t\tHtmlURL:     c.HtmlURL,\n\t}\n}\n\ntype Object struct {\n\tmodel.Object\n\tDownloadURL string\n\tHtmlURL     string\n}\n\nfunc (o *Object) URL() string {\n\treturn o.DownloadURL\n}\n\ntype Repo struct {\n\tDefaultBranch string `json:\"default_branch\"`\n}\n\ntype ErrResp struct {\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "drivers/gitee/util.go",
    "content": "package gitee\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nfunc (d *Gitee) getRepo() (*Repo, error) {\n\treq := d.client.R()\n\tif d.Token != \"\" {\n\t\treq.SetQueryParam(\"access_token\", d.Token)\n\t}\n\tif d.Cookie != \"\" {\n\t\treq.SetHeader(\"Cookie\", d.Cookie)\n\t}\n\tescapedOwner := url.PathEscape(d.Owner)\n\tescapedRepo := url.PathEscape(d.Repo)\n\tres, err := req.Get(fmt.Sprintf(\"/repos/%s/%s\", escapedOwner, escapedRepo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.IsError() {\n\t\treturn nil, toErr(res)\n\t}\n\tvar repo Repo\n\tif err := utils.Json.Unmarshal(res.Body(), &repo); err != nil {\n\t\treturn nil, err\n\t}\n\tif repo.DefaultBranch == \"\" {\n\t\treturn nil, fmt.Errorf(\"failed to fetch default branch\")\n\t}\n\treturn &repo, nil\n}\n\nfunc toErr(res *resty.Response) error {\n\tvar errMsg ErrResp\n\tif err := utils.Json.Unmarshal(res.Body(), &errMsg); err == nil && errMsg.Message != \"\" {\n\t\treturn fmt.Errorf(\"%s: %s\", res.Status(), errMsg.Message)\n\t}\n\treturn fmt.Errorf(res.Status())\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/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"time\"\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}\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}\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/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/armor\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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(\"user\").(*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\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\n// processPoint 处理单个挂载点的文件列表\nfunc (d *GithubReleases) processPoint(point *MountPoint, path string, args model.ListArgs) []File {\n\tvar pointFiles []File\n\n\tif !d.Addition.ShowAllVersion { // latest\n\t\tpoint.RequestLatestRelease(d.GetRequest, args.Refresh)\n\t\tpointFiles = d.processLatestVersion(point, path)\n\t} else { // all version\n\t\tpoint.RequestReleases(d.GetRequest, args.Refresh)\n\t\tpointFiles = d.processAllVersions(point, path)\n\t}\n\n\treturn pointFiles\n}\n\n// processLatestVersion 处理最新版本的逻辑\nfunc (d *GithubReleases) processLatestVersion(point *MountPoint, path string) []File {\n\tvar pointFiles []File\n\n\tif point.Point == path { // 与仓库路径相同\n\t\tpointFiles = append(pointFiles, point.GetLatestRelease()...)\n\t\tif d.Addition.ShowReadme {\n\t\t\tfiles := point.GetOtherFile(d.GetRequest, false)\n\t\t\tpointFiles = append(pointFiles, files...)\n\t\t}\n\t} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录\n\t\tnextDir := GetNextDir(point.Point, path)\n\t\tif nextDir != \"\" {\n\t\t\tdirFile := File{\n\t\t\t\tPath:     path + \"/\" + nextDir,\n\t\t\t\tFileName: nextDir,\n\t\t\t\tSize:     point.GetLatestSize(),\n\t\t\t\tUpdateAt: point.Release.PublishedAt,\n\t\t\t\tCreateAt: point.Release.CreatedAt,\n\t\t\t\tType:     \"dir\",\n\t\t\t\tUrl:      \"\",\n\t\t\t}\n\t\t\tpointFiles = append(pointFiles, dirFile)\n\t\t}\n\t}\n\n\treturn pointFiles\n}\n\n// processAllVersions 处理所有版本的逻辑\nfunc (d *GithubReleases) processAllVersions(point *MountPoint, path string) []File {\n\tvar pointFiles []File\n\n\tif point.Point == path { // 与仓库路径相同\n\t\tpointFiles = append(pointFiles, point.GetAllVersion()...)\n\t\tif d.Addition.ShowReadme {\n\t\t\tfiles := point.GetOtherFile(d.GetRequest, false)\n\t\t\tpointFiles = append(pointFiles, files...)\n\t\t}\n\t} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录\n\t\tnextDir := GetNextDir(point.Point, path)\n\t\tif nextDir != \"\" {\n\t\t\tdirFile := File{\n\t\t\t\tFileName: nextDir,\n\t\t\t\tPath:     path + \"/\" + nextDir,\n\t\t\t\tSize:     point.GetAllVersionSize(),\n\t\t\t\tUpdateAt: (*point.Releases)[0].PublishedAt,\n\t\t\t\tCreateAt: (*point.Releases)[0].CreatedAt,\n\t\t\t\tType:     \"dir\",\n\t\t\t\tUrl:      \"\",\n\t\t\t}\n\t\t\tpointFiles = append(pointFiles, dirFile)\n\t\t}\n\t} else if strings.HasPrefix(path, point.Point) { // 仓库目录的子目录\n\t\ttagName := GetNextDir(path, point.Point)\n\t\tif tagName != \"\" {\n\t\t\tpointFiles = append(pointFiles, point.GetReleaseByTagName(tagName)...)\n\t\t}\n\t}\n\n\treturn pointFiles\n}\n\n// mergeFiles 合并文件列表，处理重复目录\nfunc (d *GithubReleases) mergeFiles(files *[]File, newFiles []File) {\n\tfor _, newFile := range newFiles {\n\t\tif newFile.Type == \"dir\" {\n\t\t\thasSameDir := false\n\t\t\tfor index := range *files {\n\t\t\t\tif (*files)[index].GetName() == newFile.GetName() && (*files)[index].Type == \"dir\" {\n\t\t\t\t\thasSameDir = true\n\t\t\t\t\t(*files)[index].Size += newFile.Size\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !hasSameDir {\n\t\t\t\t*files = append(*files, newFile)\n\t\t\t}\n\t\t} else {\n\t\t\t*files = append(*files, newFile)\n\t\t}\n\t}\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\tif d.Addition.ConcurrentRequests && d.Addition.Token != \"\" { // 并发处理\n\t\tvar mu sync.Mutex\n\t\tvar wg sync.WaitGroup\n\n\t\tfor i := range d.points {\n\t\t\twg.Add(1)\n\t\t\tgo func(point *MountPoint) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tpointFiles := d.processPoint(point, path, args)\n\n\t\t\t\tmu.Lock()\n\t\t\t\td.mergeFiles(&files, pointFiles)\n\t\t\t\tmu.Unlock()\n\t\t\t}(&d.points[i])\n\t\t}\n\t\twg.Wait()\n\t} else { // 串行处理\n\t\tfor i := range d.points {\n\t\t\tpoint := &d.points[i]\n\t\t\tpointFiles := d.processPoint(point, path, args)\n\t\t\td.mergeFiles(&files, pointFiles)\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tRepoStructure      string `json:\"repo_structure\" type:\"text\" required:\"true\" default:\"alistGo/alist\" 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\tShowAllVersion     bool   `json:\"show_all_version\" type:\"bool\" default:\"false\" help:\"show all versions\"`\n\tConcurrentRequests bool   `json:\"concurrent_requests\" type:\"bool\" default:\"false\" help:\"To concurrently request the GitHub API, you must enter a GitHub token\"`\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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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) RequestLatestRelease(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)\n\tfor _, asset := range m.Release.Assets {\n\t\tfiles = append(files, File{\n\t\t\tPath:     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:     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:     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) 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:     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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\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\tutils.Log.Warnf(\"failed to get request: %s %d %s\", url, 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/gofile/driver.go",
    "content": "package gofile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Gofile struct {\n\tmodel.Storage\n\tAddition\n\n\taccountId string\n}\n\nfunc (d *Gofile) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Gofile) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Gofile) Init(ctx context.Context) error {\n\tif d.APIToken == \"\" {\n\t\treturn fmt.Errorf(\"API token is required\")\n\t}\n\n\t// Get account ID\n\taccountId, err := d.getAccountId(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get account ID: %w\", err)\n\t}\n\td.accountId = accountId\n\n\t// Get account info to set root folder if not specified\n\tif d.RootFolderID == \"\" {\n\t\taccountInfo, err := d.getAccountInfo(ctx, accountId)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get account info: %w\", err)\n\t\t}\n\t\td.RootFolderID = accountInfo.Data.RootFolder\n\t}\n\n\t// Save driver storage\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Gofile) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Gofile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar folderId string\n\tif dir.GetID() == \"\" {\n\t\tfolderId = d.GetRootId()\n\t} else {\n\t\tfolderId = dir.GetID()\n\t}\n\n\tendpoint := fmt.Sprintf(\"/contents/%s\", folderId)\n\n\tvar response ContentsResponse\n\terr := d.getJSON(ctx, endpoint, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar objects []model.Obj\n\n\t// Process children or contents\n\tcontents := response.Data.Children\n\tif contents == nil {\n\t\tcontents = response.Data.Contents\n\t}\n\n\tfor _, content := range contents {\n\t\tobjects = append(objects, d.convertContentToObj(content))\n\t}\n\n\treturn objects, nil\n}\n\nfunc (d *Gofile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.IsDir() {\n\t\treturn nil, errs.NotFile\n\t}\n\n\t// Create a direct link for the file\n\tdirectLink, err := d.createDirectLink(ctx, file.GetID())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create direct link: %w\", err)\n\t}\n\n\t// Configure cache expiration based on user setting\n\tlink := &model.Link{\n\t\tURL: directLink,\n\t}\n\n\t// Only set expiration if LinkExpiry > 0 (0 means no caching)\n\tif d.LinkExpiry > 0 {\n\t\texpiration := time.Duration(d.LinkExpiry) * 24 * time.Hour\n\t\tlink.Expiration = &expiration\n\t}\n\n\treturn link, nil\n}\n\nfunc (d *Gofile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar parentId string\n\tif parentDir.GetID() == \"\" {\n\t\tparentId = d.GetRootId()\n\t} else {\n\t\tparentId = parentDir.GetID()\n\t}\n\n\tdata := map[string]interface{}{\n\t\t\"parentFolderId\": parentId,\n\t\t\"folderName\":     dirName,\n\t}\n\n\tvar response CreateFolderResponse\n\terr := d.postJSON(ctx, \"/contents/createFolder\", data, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       response.Data.ID,\n\t\tName:     response.Data.Name,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (d *Gofile) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar dstId string\n\tif dstDir.GetID() == \"\" {\n\t\tdstId = d.GetRootId()\n\t} else {\n\t\tdstId = dstDir.GetID()\n\t}\n\n\tdata := map[string]interface{}{\n\t\t\"contentsId\": srcObj.GetID(),\n\t\t\"folderId\":   dstId,\n\t}\n\n\terr := d.putJSON(ctx, \"/contents/move\", data, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Return updated object\n\treturn &model.Object{\n\t\tID:       srcObj.GetID(),\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 *Gofile) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tdata := map[string]interface{}{\n\t\t\"attribute\":      \"name\",\n\t\t\"attributeValue\": newName,\n\t}\n\n\tvar response UpdateResponse\n\terr := d.putJSON(ctx, fmt.Sprintf(\"/contents/%s/update\", srcObj.GetID()), data, &response)\n\tif 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\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *Gofile) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar dstId string\n\tif dstDir.GetID() == \"\" {\n\t\tdstId = d.GetRootId()\n\t} else {\n\t\tdstId = dstDir.GetID()\n\t}\n\n\tdata := map[string]interface{}{\n\t\t\"contentsId\": srcObj.GetID(),\n\t\t\"folderId\":   dstId,\n\t}\n\n\tvar response CopyResponse\n\terr := d.postJSON(ctx, \"/contents/copy\", data, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the new ID from the response\n\tnewId := srcObj.GetID()\n\tif response.Data.CopiedContents != nil {\n\t\tif id, ok := response.Data.CopiedContents[srcObj.GetID()]; ok {\n\t\t\tnewId = id\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\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *Gofile) Remove(ctx context.Context, obj model.Obj) error {\n\tdata := map[string]interface{}{\n\t\t\"contentsId\": obj.GetID(),\n\t}\n\n\treturn d.deleteJSON(ctx, \"/contents\", data)\n}\n\nfunc (d *Gofile) Put(ctx context.Context, dstDir model.Obj, fileStreamer model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tvar folderId string\n\tif dstDir.GetID() == \"\" {\n\t\tfolderId = d.GetRootId()\n\t} else {\n\t\tfolderId = dstDir.GetID()\n\t}\n\n\tresponse, err := d.uploadFile(ctx, folderId, fileStreamer, up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       response.Data.FileId,\n\t\tName:     response.Data.FileName,\n\t\tSize:     fileStreamer.GetSize(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\nfunc (d *Gofile) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Gofile) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Gofile) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Gofile) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nvar _ driver.Driver = (*Gofile)(nil)\n"
  },
  {
    "path": "drivers/gofile/meta.go",
    "content": "package gofile\n\nimport (\n    \"github.com/alist-org/alist/v3/internal/driver\"\n    \"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n    driver.RootID\n    APIToken         string `json:\"api_token\" required:\"true\" help:\"Get your API token from your Gofile profile page\"`\n    LinkExpiry       int    `json:\"link_expiry\" type:\"number\" default:\"30\" help:\"Direct link cache duration in days. Set to 0 to disable caching\"`\n    DirectLinkExpiry int    `json:\"direct_link_expiry\" type:\"number\" default:\"0\" help:\"Direct link expiration time in hours on Gofile server. Set to 0 for no expiration\"`\n}\n\nvar config = driver.Config{\n    Name:        \"Gofile\",\n    DefaultRoot: \"\",\n    LocalSort:   false,\n    OnlyProxy:   false,\n    NoCache:     false,\n    NoUpload:    false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Gofile{}\n\t})\n}\n"
  },
  {
    "path": "drivers/gofile/types.go",
    "content": "package gofile\n\nimport \"time\"\n\ntype APIResponse struct {\n\tStatus string      `json:\"status\"`\n\tData   interface{} `json:\"data\"`\n}\n\ntype AccountResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tID string `json:\"id\"`\n\t} `json:\"data\"`\n}\n\ntype AccountInfoResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tID         string `json:\"id\"`\n\t\tType       string `json:\"type\"`\n\t\tEmail      string `json:\"email\"`\n\t\tRootFolder string `json:\"rootFolder\"`\n\t} `json:\"data\"`\n}\n\ntype Content struct {\n\tID           string             `json:\"id\"`\n\tType         string             `json:\"type\"` // \"file\" or \"folder\"\n\tName         string             `json:\"name\"`\n\tSize         int64              `json:\"size,omitempty\"`\n\tCreateTime   int64              `json:\"createTime\"`\n\tModTime      int64              `json:\"modTime,omitempty\"`\n\tDirectLink   string             `json:\"directLink,omitempty\"`\n\tChildren     map[string]Content `json:\"children,omitempty\"`\n\tParentFolder string             `json:\"parentFolder,omitempty\"`\n\tMD5          string             `json:\"md5,omitempty\"`\n\tMimeType     string             `json:\"mimeType,omitempty\"`\n\tLink         string             `json:\"link,omitempty\"`\n}\n\ntype ContentsResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tIsOwner      bool               `json:\"isOwner\"`\n\t\tID           string             `json:\"id\"`\n\t\tType         string             `json:\"type\"`\n\t\tName         string             `json:\"name\"`\n\t\tParentFolder string             `json:\"parentFolder\"`\n\t\tCreateTime   int64              `json:\"createTime\"`\n\t\tChildrenList []string           `json:\"childrenList,omitempty\"`\n\t\tChildren     map[string]Content `json:\"children,omitempty\"`\n\t\tContents     map[string]Content `json:\"contents,omitempty\"`\n\t\tPublic       bool               `json:\"public,omitempty\"`\n\t\tDescription  string             `json:\"description,omitempty\"`\n\t\tTags         string             `json:\"tags,omitempty\"`\n\t\tExpiry       int64              `json:\"expiry,omitempty\"`\n\t} `json:\"data\"`\n}\n\ntype UploadResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tDownloadPage string `json:\"downloadPage\"`\n\t\tCode         string `json:\"code\"`\n\t\tParentFolder string `json:\"parentFolder\"`\n\t\tFileId       string `json:\"fileId\"`\n\t\tFileName     string `json:\"fileName\"`\n\t\tGuestToken   string `json:\"guestToken,omitempty\"`\n\t} `json:\"data\"`\n}\n\ntype DirectLinkResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tDirectLink string `json:\"directLink\"`\n\t\tID         string `json:\"id\"`\n\t} `json:\"data\"`\n}\n\ntype CreateFolderResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tID           string `json:\"id\"`\n\t\tType         string `json:\"type\"`\n\t\tName         string `json:\"name\"`\n\t\tParentFolder string `json:\"parentFolder\"`\n\t\tCreateTime   int64  `json:\"createTime\"`\n\t} `json:\"data\"`\n}\n\ntype CopyResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tCopiedContents map[string]string `json:\"copiedContents\"` // oldId -> newId mapping\n\t} `json:\"data\"`\n}\n\ntype UpdateResponse struct {\n\tStatus string `json:\"status\"`\n\tData   struct {\n\t\tID   string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t} `json:\"data\"`\n}\n\ntype ErrorResponse struct {\n\tStatus string `json:\"status\"`\n\tError  struct {\n\t\tMessage string `json:\"message\"`\n\t\tCode    string `json:\"code\"`\n\t} `json:\"error\"`\n}\n\nfunc (c *Content) ModifiedTime() time.Time {\n\tif c.ModTime > 0 {\n\t\treturn time.Unix(c.ModTime, 0)\n\t}\n\treturn time.Unix(c.CreateTime, 0)\n}\n\nfunc (c *Content) IsDir() bool {\n\treturn c.Type == \"folder\"\n}\n"
  },
  {
    "path": "drivers/gofile/util.go",
    "content": "package gofile\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\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tbaseAPI   = \"https://api.gofile.io\"\n\tuploadAPI = \"https://upload.gofile.io\"\n)\n\nfunc (d *Gofile) request(ctx context.Context, method, endpoint string, body io.Reader, headers map[string]string) (*http.Response, error) {\n\tvar url string\n\tif strings.HasPrefix(endpoint, \"http\") {\n\t\turl = endpoint\n\t} else {\n\t\turl = baseAPI + endpoint\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+d.APIToken)\n\treq.Header.Set(\"User-Agent\", \"AList/3.0\")\n\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\treturn base.HttpClient.Do(req)\n}\n\nfunc (d *Gofile) getJSON(ctx context.Context, endpoint string, result interface{}) error {\n\tresp, err := d.request(ctx, \"GET\", endpoint, nil, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn d.handleError(resp)\n\t}\n\n\treturn json.NewDecoder(resp.Body).Decode(result)\n}\n\nfunc (d *Gofile) postJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {\n\tjsonData, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theaders := map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}\n\n\tresp, err := d.request(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData), headers)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn d.handleError(resp)\n\t}\n\n\tif result != nil {\n\t\treturn json.NewDecoder(resp.Body).Decode(result)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Gofile) putJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {\n\tjsonData, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theaders := map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}\n\n\tresp, err := d.request(ctx, \"PUT\", endpoint, bytes.NewBuffer(jsonData), headers)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn d.handleError(resp)\n\t}\n\n\tif result != nil {\n\t\treturn json.NewDecoder(resp.Body).Decode(result)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Gofile) deleteJSON(ctx context.Context, endpoint string, data interface{}) error {\n\tjsonData, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theaders := map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}\n\n\tresp, err := d.request(ctx, \"DELETE\", endpoint, bytes.NewBuffer(jsonData), headers)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn d.handleError(resp)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Gofile) handleError(resp *http.Response) error {\n\tbody, _ := io.ReadAll(resp.Body)\n\tlog.Debugf(\"Gofile API error (HTTP %d): %s\", resp.StatusCode, string(body))\n\n\tvar errorResp ErrorResponse\n\tif err := json.Unmarshal(body, &errorResp); err == nil && errorResp.Status == \"error\" {\n\t\treturn fmt.Errorf(\"gofile API error: %s (code: %s)\", errorResp.Error.Message, errorResp.Error.Code)\n\t}\n\n\treturn fmt.Errorf(\"gofile API error: HTTP %d - %s\", resp.StatusCode, string(body))\n}\n\nfunc (d *Gofile) uploadFile(ctx context.Context, folderId string, file model.FileStreamer, up driver.UpdateProgress) (*UploadResponse, error) {\n\tvar body bytes.Buffer\n\twriter := multipart.NewWriter(&body)\n\n\tif folderId != \"\" {\n\t\twriter.WriteField(\"folderId\", folderId)\n\t}\n\n\tpart, err := writer.CreateFormFile(\"file\", filepath.Base(file.GetName()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Copy with progress tracking if available\n\tif up != nil {\n\t\treader := &progressReader{\n\t\t\treader: file,\n\t\t\ttotal:  file.GetSize(),\n\t\t\tup:     up,\n\t\t}\n\t\t_, err = io.Copy(part, reader)\n\t} else {\n\t\t_, err = io.Copy(part, file)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twriter.Close()\n\n\theaders := map[string]string{\n\t\t\"Content-Type\": writer.FormDataContentType(),\n\t}\n\n\tresp, err := d.request(ctx, \"POST\", uploadAPI+\"/uploadfile\", &body, headers)\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, d.handleError(resp)\n\t}\n\n\tvar result UploadResponse\n\terr = json.NewDecoder(resp.Body).Decode(&result)\n\treturn &result, err\n}\n\nfunc (d *Gofile) createDirectLink(ctx context.Context, contentId string) (string, error) {\n\tdata := map[string]interface{}{}\n\n\tif d.DirectLinkExpiry > 0 {\n\t\texpireTime := time.Now().Add(time.Duration(d.DirectLinkExpiry) * time.Hour).Unix()\n\t\tdata[\"expireTime\"] = expireTime\n\t}\n\n\tvar result DirectLinkResponse\n\terr := d.postJSON(ctx, fmt.Sprintf(\"/contents/%s/directlinks\", contentId), data, &result)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn result.Data.DirectLink, nil\n}\n\nfunc (d *Gofile) convertContentToObj(content Content) model.Obj {\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       content.ID,\n\t\t\tName:     content.Name,\n\t\t\tSize:     content.Size,\n\t\t\tModified: content.ModifiedTime(),\n\t\t\tIsFolder: content.IsDir(),\n\t\t},\n\t}\n}\n\nfunc (d *Gofile) getAccountId(ctx context.Context) (string, error) {\n\tvar result AccountResponse\n\terr := d.getJSON(ctx, \"/accounts/getid\", &result)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn result.Data.ID, nil\n}\n\nfunc (d *Gofile) getAccountInfo(ctx context.Context, accountId string) (*AccountInfoResponse, error) {\n\tvar result AccountInfoResponse\n\terr := d.getJSON(ctx, fmt.Sprintf(\"/accounts/%s\", accountId), &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &result, nil\n}\n\n// progressReader wraps an io.Reader to track upload progress\ntype progressReader struct {\n\treader io.Reader\n\ttotal  int64\n\tread   int64\n\tup     driver.UpdateProgress\n}\n\nfunc (pr *progressReader) Read(p []byte) (n int, err error) {\n\tn, err = pr.reader.Read(p)\n\tpr.read += int64(n)\n\tif pr.up != nil && pr.total > 0 {\n\t\tprogress := float64(pr.read) * 100 / float64(pr.total)\n\t\tpr.up(progress)\n\t}\n\treturn n, err\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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)\n\t}\n\treturn err\n}\n\nvar _ driver.Driver = (*GoogleDrive)(nil)\n"
  },
  {
    "path": "drivers/google_drive/meta.go",
    "content": "package google_drive\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tClientID       string `json:\"client_id\" required:\"true\" default:\"202264815644.apps.googleusercontent.com\"`\n\tClientSecret   string `json:\"client_secret\" required:\"true\" default:\"X4Z3ca8xfWDb1Voo-F9a7ZxJ\"`\n\tChunkSize      int64  `json:\"chunk_size\" type:\"number\" default:\"5\" help:\"chunk size while uploading (unit: MB)\"`\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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"
  },
  {
    "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\"net/http\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/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\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// 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\":   \"files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken\",\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\t\tres = append(res, resp.Files...)\n\t}\n\treturn res, nil\n}\n\nfunc (d *GoogleDrive) chunkUpload(ctx context.Context, stream model.FileStreamer, url string) error {\n\tvar defaultChunkSize = d.ChunkSize * 1024 * 1024\n\tvar offset int64 = 0\n\tfor offset < stream.GetSize() {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tchunkSize := stream.GetSize() - offset\n\t\tif chunkSize > defaultChunkSize {\n\t\t\tchunkSize = defaultChunkSize\n\t\t}\n\t\treader, err := stream.RangeRead(http_range.Range{Start: offset, Length: chunkSize})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treader = driver.NewLimitedUploadStream(ctx, reader)\n\t\t_, err = d.request(url, http.MethodPut, func(req *resty.Request) {\n\t\t\treq.SetHeaders(map[string]string{\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, stream.GetSize()),\n\t\t\t}).SetBody(reader).SetContext(ctx)\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\toffset += chunkSize\n\t}\n\treturn 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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/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: \"全部媒体\",\n\t\t},\n\t\t{\n\t\t\tId: FETCH_ALBUMS,\n\t\t\tTitle: \"全部影集\",\n\t\t},\n\t\t{\n\t\t\tId: FETCH_SHARE_ALBUMS,\n\t\t\tTitle: \"共享影集\",\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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 = -1\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"open download file failed: %w\", err)\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\tresultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader}\n\treturn &model.Link{\n\t\tRangeReadCloser: resultRangeReadCloser,\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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:\"alist/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\tLocalSort:         false,\n\tOnlyLocal:         true,\n\tOnlyProxy:         true,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\t\"time\"\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\nfunc (s *SteamFile) Close() error {\n\treturn s.file.Close()\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\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/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/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\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0\",\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.CacheFullInTempFileAndHash(s, 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\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tLocalSort:         false,\n\t\t\t\tOnlyLocal:         false,\n\t\t\t\tOnlyProxy:         false,\n\t\t\t\tNoCache:           false,\n\t\t\t\tNoUpload:          false,\n\t\t\t\tNeedMs:            false,\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tCheckStatus:       false,\n\t\t\t\tAlert:             \"\",\n\t\t\t\tNoOverwriteUpload: false,\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\tLocalSort:         false,\n\t\t\t\tOnlyLocal:         false,\n\t\t\t\tOnlyProxy:         false,\n\t\t\t\tNoCache:           false,\n\t\t\t\tNoUpload:          false,\n\t\t\t\tNeedMs:            false,\n\t\t\t\tDefaultRoot:       \"0\",\n\t\t\t\tCheckStatus:       false,\n\t\t\t\tAlert:             \"\",\n\t\t\t\tNoOverwriteUpload: false,\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 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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\"User-Agent\":      \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0\",\n\t\t\"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n\t\t\"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,mt;q=0.5\",\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyProxy:   false,\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tDefaultRoot: \"\",\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\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"strings\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.39 (KHTML, like Gecko) Chrome/89.0.4389.111 Safari/537.39\"\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\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\tlog \"github.com/sirupsen/logrus\"\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 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\tif inComment || inSingleLineComment {\n\t\t\tcontinue\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(html string) (string, error) {\n\tlog.Debugln(\"acw_sc__v2\", html)\n\tacwScV2s := findAcwScV2Reg.FindStringSubmatch(html)\n\tif len(acwScV2s) != 2 {\n\t\treturn \"\", fmt.Errorf(\"无法匹配acw_sc__v2\")\n\t}\n\treturn HexXor(Unbox(acwScV2s[1]), \"3000176000856006061501533003690027800375\"), 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 := 0; i < len(box); i++ {\n\t\tj := box[i]\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 {\n\tout := bytes.NewBuffer(make([]byte, len(hex1)))\n\tfor i := 0; i < len(hex1) && i < len(hex2); i += 2 {\n\t\tv1, _ := strconv.ParseInt(hex1[i:i+2], 16, 64)\n\t\tv2, _ := strconv.ParseInt(hex2[i:i+2], 16, 64)\n\t\tout.WriteString(strconv.FormatInt(v1^v2, 16))\n\t}\n\treturn out.String()\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/89.0.4389.111 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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"time\"\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\"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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\nfunc (d *LanZou) request(url string, method string, callback base.ReqCallback, up bool) ([]byte, error) {\n\tvar req *resty.Request\n\tif up {\n\t\tonce.Do(func() {\n\t\t\tupClient = base.NewRestyClient().SetTimeout(120 * time.Second)\n\t\t})\n\t\treq = upClient.R()\n\t} else {\n\t\treq = base.RestyClient.R()\n\t}\n\n\treq.SetHeaders(map[string]string{\n\t\t\"Referer\":    \"https://pc.woozooo.com\",\n\t\t\"User-Agent\": d.UserAgent,\n\t})\n\n\tif d.Cookie != \"\" {\n\t\treq.SetHeader(\"cookie\", d.Cookie)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"lanzou request: url=>%s ,stats=>%d ,body => %s\\n\", res.Request.URL, res.StatusCode(), res.String())\n\treturn res.Body(), err\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\theaders := map[string]string{\n\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}\n\tres, err := base.NoRedirectClient.R().SetHeaders(headers).Get(downloadUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trPageData := res.String()\n\tif findAcwScV2Reg.MatchString(rPageData) {\n\t\tlog.Debug(\"lanzou: detected acw_sc__v2 challenge, recalculating cookie\")\n\t\tacwScV2, err := CalcAcwScV2(rPageData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// retry with calculated cookie to bypass anti-crawler validation\n\t\tres, err = base.NoRedirectClient.R().\n\t\t\tSetHeaders(headers).\n\t\t\tSetCookie(&http.Cookie{Name: \"acw_sc__v2\", Value: acwScV2}).\n\t\t\tGet(downloadUrl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trPageData = res.String()\n\t}\n\n\tfile.Url = res.Header().Get(\"location\")\n\n\t// 触发验证\n\tif res.StatusCode() != 302 {\n\t\tparam, err = htmlJsonToMap(rPageData)\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\tdata, err := d.post(fmt.Sprint(baseUrl, \"/ajax.php\"), func(req *resty.Request) { req.SetFormData(param) }, nil)\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/lark/driver.go",
    "content": "package lark\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\tlark \"github.com/larksuite/oapi-sdk-go/v3\"\n\tlarkcore \"github.com/larksuite/oapi-sdk-go/v3/core\"\n\tlarkdrive \"github.com/larksuite/oapi-sdk-go/v3/service/drive/v1\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype Lark struct {\n\tmodel.Storage\n\tAddition\n\n\tclient          *lark.Client\n\trootFolderToken string\n}\n\nfunc (c *Lark) Config() driver.Config {\n\treturn config\n}\n\nfunc (c *Lark) GetAddition() driver.Additional {\n\treturn &c.Addition\n}\n\nfunc (c *Lark) Init(ctx context.Context) error {\n\tc.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))\n\n\tpaths := strings.Split(c.RootFolderPath, \"/\")\n\ttoken := \"\"\n\n\tvar ok bool\n\tvar file *larkdrive.File\n\tfor _, p := range paths {\n\t\tif p == \"\" {\n\t\t\ttoken = \"\"\n\t\t\tcontinue\n\t\t}\n\n\t\tresp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor {\n\t\t\tok, file, err = resp.Next()\n\t\t\tif !ok {\n\t\t\t\treturn errs.ObjectNotFound\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif *file.Type == \"folder\" && *file.Name == p {\n\t\t\t\ttoken = *file.Token\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tc.rootFolderToken = token\n\n\treturn nil\n}\n\nfunc (c *Lark) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\ttoken, ok := c.getObjToken(ctx, dir.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tif token == emptyFolderToken {\n\t\treturn nil, nil\n\t}\n\n\tresp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tok = false\n\tvar file *larkdrive.File\n\tvar res []model.Obj\n\n\tfor {\n\t\tok, file, err = resp.Next()\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmodifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)\n\t\tcreatedUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)\n\n\t\tf := model.Object{\n\t\t\tID:       *file.Token,\n\t\t\tPath:     strings.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}, \"/\"),\n\t\t\tName:     *file.Name,\n\t\t\tSize:     0,\n\t\t\tModified: time.Unix(modifiedUnix, 0),\n\t\t\tCtime:    time.Unix(createdUnix, 0),\n\t\t\tIsFolder: *file.Type == \"folder\",\n\t\t}\n\t\tres = append(res, &f)\n\t}\n\n\treturn res, nil\n}\n\nfunc (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\ttoken, ok := c.getObjToken(ctx, file.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tresp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{\n\t\tAppID:     c.AppId,\n\t\tAppSecret: c.AppSecret,\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !c.ExternalMode {\n\t\taccessToken := resp.TenantAccessToken\n\n\t\turl := fmt.Sprintf(\"https://open.feishu.cn/open-apis/drive/v1/files/%s/download\", token)\n\n\t\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\t\treq.Header.Set(\"Range\", \"bytes=0-1\")\n\n\t\tar, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif ar.StatusCode != http.StatusPartialContent {\n\t\t\treturn nil, errors.New(\"failed to get download link\")\n\t\t}\n\n\t\treturn &model.Link{\n\t\t\tURL: url,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Authorization\": []string{fmt.Sprintf(\"Bearer %s\", accessToken)},\n\t\t\t},\n\t\t}, nil\n\t} else {\n\t\turl := strings.Join([]string{c.TenantUrlPrefix, \"file\", token}, \"/\")\n\n\t\treturn &model.Link{\n\t\t\tURL: url,\n\t\t}, nil\n\t}\n}\n\nfunc (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\ttoken, ok := c.getObjToken(ctx, parentDir.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tbody, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := c.client.Drive.File.CreateFolder(ctx,\n\t\tlarkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !resp.Success() {\n\t\treturn nil, errors.New(resp.Error())\n\t}\n\n\treturn &model.Object{\n\t\tID:       *resp.Data.Token,\n\t\tPath:     strings.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}, \"/\"),\n\t\tName:     dirName,\n\t\tSize:     0,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tsrcToken, ok := c.getObjToken(ctx, srcObj.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tdstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\treq := larkdrive.NewMoveFileReqBuilder().\n\t\tBody(larkdrive.NewMoveFileReqBodyBuilder().\n\t\t\tType(\"file\").\n\t\t\tFolderToken(dstDirToken).\n\t\t\tBuild()).FileToken(srcToken).\n\t\tBuild()\n\n\t// 发起请求\n\tresp, err := c.client.Drive.File.Move(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !resp.Success() {\n\t\treturn nil, errors.New(resp.Error())\n\t}\n\n\treturn nil, nil\n}\n\nfunc (c *Lark) 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 (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tsrcToken, ok := c.getObjToken(ctx, srcObj.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tdstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\treq := larkdrive.NewCopyFileReqBuilder().\n\t\tBody(larkdrive.NewCopyFileReqBodyBuilder().\n\t\t\tName(srcObj.GetName()).\n\t\t\tType(\"file\").\n\t\t\tFolderToken(dstDirToken).\n\t\t\tBuild()).FileToken(srcToken).\n\t\tBuild()\n\n\t// 发起请求\n\tresp, err := c.client.Drive.File.Copy(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !resp.Success() {\n\t\treturn nil, errors.New(resp.Error())\n\t}\n\n\treturn nil, nil\n}\n\nfunc (c *Lark) Remove(ctx context.Context, obj model.Obj) error {\n\ttoken, ok := c.getObjToken(ctx, obj.GetPath())\n\tif !ok {\n\t\treturn errs.ObjectNotFound\n\t}\n\n\treq := larkdrive.NewDeleteFileReqBuilder().\n\t\tFileToken(token).\n\t\tType(\"file\").\n\t\tBuild()\n\n\t// 发起请求\n\tresp, err := c.client.Drive.File.Delete(ctx, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !resp.Success() {\n\t\treturn errors.New(resp.Error())\n\t}\n\n\treturn nil\n}\n\nvar uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)\n\nfunc (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\ttoken, ok := c.getObjToken(ctx, dstDir.GetPath())\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\t// prepare\n\treq := larkdrive.NewUploadPrepareFileReqBuilder().\n\t\tFileUploadInfo(larkdrive.NewFileUploadInfoBuilder().\n\t\t\tFileName(stream.GetName()).\n\t\t\tParentType(`explorer`).\n\t\t\tParentNode(token).\n\t\t\tSize(int(stream.GetSize())).\n\t\t\tBuild()).\n\t\tBuild()\n\n\t// 发起请求\n\terr := uploadLimit.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := c.client.Drive.File.UploadPrepare(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !resp.Success() {\n\t\treturn nil, errors.New(resp.Error())\n\t}\n\n\tuploadId := *resp.Data.UploadId\n\tblockSize := *resp.Data.BlockSize\n\tblockCount := *resp.Data.BlockNum\n\n\t// upload\n\tfor i := 0; i < blockCount; i++ {\n\t\tlength := int64(blockSize)\n\t\tif i == blockCount-1 {\n\t\t\tlength = stream.GetSize() - int64(i*blockSize)\n\t\t}\n\n\t\treader := driver.NewLimitedUploadStream(ctx, io.LimitReader(stream, length))\n\n\t\treq := larkdrive.NewUploadPartFileReqBuilder().\n\t\t\tBody(larkdrive.NewUploadPartFileReqBodyBuilder().\n\t\t\t\tUploadId(uploadId).\n\t\t\t\tSeq(i).\n\t\t\t\tSize(int(length)).\n\t\t\t\tFile(reader).\n\t\t\t\tBuild()).\n\t\t\tBuild()\n\n\t\t// 发起请求\n\t\terr = uploadLimit.Wait(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp, err := c.client.Drive.File.UploadPart(ctx, req)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif !resp.Success() {\n\t\t\treturn nil, errors.New(resp.Error())\n\t\t}\n\n\t\tup(float64(i) / float64(blockCount))\n\t}\n\n\t//close\n\tcloseReq := larkdrive.NewUploadFinishFileReqBuilder().\n\t\tBody(larkdrive.NewUploadFinishFileReqBodyBuilder().\n\t\t\tUploadId(uploadId).\n\t\t\tBlockNum(blockCount).\n\t\t\tBuild()).\n\t\tBuild()\n\n\t// 发起请求\n\tcloseResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !closeResp.Success() {\n\t\treturn nil, errors.New(closeResp.Error())\n\t}\n\n\treturn &model.Object{\n\t\tID: *closeResp.Data.FileToken,\n\t}, nil\n}\n\n//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Lark)(nil)\n"
  },
  {
    "path": "drivers/lark/meta.go",
    "content": "package lark\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\tdriver.RootPath\n\t// define other\n\tAppId           string `json:\"app_id\" type:\"text\" help:\"app id\"`\n\tAppSecret       string `json:\"app_secret\" type:\"text\" help:\"app secret\"`\n\tExternalMode    bool   `json:\"external_mode\" type:\"bool\" help:\"external mode\"`\n\tTenantUrlPrefix string `json:\"tenant_url_prefix\" type:\"text\" help:\"tenant url prefix\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Lark\",\n\tLocalSort:         false,\n\tOnlyLocal:         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 &Lark{}\n\t})\n}\n"
  },
  {
    "path": "drivers/lark/types.go",
    "content": "package lark\n\nimport (\n\t\"context\"\n\t\"github.com/Xhofe/go-cache\"\n\t\"time\"\n)\n\ntype TokenCache struct {\n\tcache.ICache[string]\n}\n\nfunc (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {\n\tt.ICache.Set(key, value, cache.WithEx[string](expireTime))\n\n\treturn nil\n}\n\nfunc (t *TokenCache) Get(_ context.Context, key string) (string, error) {\n\tv, ok := t.ICache.Get(key)\n\tif ok {\n\t\treturn v, nil\n\t}\n\n\treturn \"\", nil\n}\n\nfunc newTokenCache() *TokenCache {\n\tc := cache.NewMemCache[string]()\n\n\treturn &TokenCache{c}\n}\n"
  },
  {
    "path": "drivers/lark/util.go",
    "content": "package lark\n\nimport (\n\t\"context\"\n\t\"github.com/Xhofe/go-cache\"\n\tlarkdrive \"github.com/larksuite/oapi-sdk-go/v3/service/drive/v1\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"path\"\n\t\"time\"\n)\n\nconst objTokenCacheDuration = 5 * time.Minute\nconst emptyFolderToken = \"empty\"\n\nvar objTokenCache = cache.NewMemCache[string]()\nvar exOpts = cache.WithEx[string](objTokenCacheDuration)\n\nfunc (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {\n\tif token, ok := objTokenCache.Get(folderPath); ok {\n\t\treturn token, true\n\t}\n\n\tdir, name := path.Split(folderPath)\n\t// strip the last slash of dir if it exists\n\tif len(dir) > 0 && dir[len(dir)-1] == '/' {\n\t\tdir = dir[:len(dir)-1]\n\t}\n\tif name == \"\" {\n\t\treturn c.rootFolderToken, true\n\t}\n\n\tvar parentToken string\n\tvar found bool\n\tparentToken, found = c.getObjToken(ctx, dir)\n\tif !found {\n\t\treturn emptyFolderToken, false\n\t}\n\n\treq := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()\n\tresp, err := c.client.Drive.File.ListByIterator(ctx, req)\n\n\tif err != nil {\n\t\tlog.WithError(err).Error(\"failed to list files\")\n\t\treturn emptyFolderToken, false\n\t}\n\n\tvar file *larkdrive.File\n\tfor {\n\t\tfound, file, err = resp.Next()\n\t\tif !found {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.WithError(err).Error(\"failed to get next file\")\n\t\t\tbreak\n\t\t}\n\n\t\tif *file.Name == name {\n\t\t\tobjTokenCache.Set(folderPath, *file.Token, exOpts)\n\t\t\treturn *file.Token, true\n\t\t}\n\t}\n\n\treturn emptyFolderToken, false\n}\n"
  },
  {
    "path": "drivers/lark.go",
    "content": "// +build linux darwin windows\n// +build amd64 arm64\n\npackage drivers\n\nimport (\n\t_ \"github.com/alist-org/alist/v3/drivers/lark\"\n)\n"
  },
  {
    "path": "drivers/lenovonas_share/driver.go",
    "content": "package LenovoNasShare\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\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\tfiles := make([]File, 0)\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\":   dir.GetPath(),\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\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles = append(files, resp.Data.List...)\n\n\treturn utils.SliceConvert(files, func(src File) (model.Obj, error) {\n\t\treturn src, 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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\n\nvar config = driver.Config{\n\tName:              \"LenovoNasShare\",\n\tLocalSort:         true,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          true,\n\tNeedMs:            false,\n\tDefaultRoot:       \"\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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/alist-org/alist/v3/pkg/utils\"\n\n\t_ \"github.com/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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) alist-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/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/times\"\n\tcp \"github.com/otiai10/copy\"\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\tthumbSize int\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\tthumbPixel                int\n\n\t// use ffmpeg\n\tuseFFmpeg 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 = 0777\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\n\td.useFFmpeg = d.UseFFmpeg\n\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\td.thumbSize = 144\n\tif item, err := op.GetSettingItemByKey(conf.ThumbnailSize); err == nil && item != nil && strings.TrimSpace(item.Value) != \"\" {\n\t\tv, err := strconv.ParseUint(item.Value, 10, 32)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid setting %s value: %s, err: %s\", conf.ThumbnailSize, item.Value, err)\n\t\t}\n\t\tif v == 0 {\n\t\t\treturn fmt.Errorf(\"invalid setting %s value: %s, the value must be a positive integer\", conf.ThumbnailSize, item.Value)\n\t\t}\n\t\td.thumbSize = int(v)\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.ThumbPixel != \"\" {\n\t\tv, err := strconv.ParseUint(d.ThumbPixel, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td.thumbPixel = int(v)\n\t}\n\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 err != nil {\n\t\treturn nil, err\n\t}\n\tvar files []model.Obj\n\tfor _, f := range rawFiles {\n\t\tif !d.ShowHidden && strings.HasPrefix(f.Name(), \".\") {\n\t\t\tcontinue\n\t\t}\n\t\tfile := d.FileInfoToObj(ctx, f, args.ReqPath, fullPath)\n\t\tfiles = append(files, file)\n\t}\n\treturn files, nil\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(common.GetHttpReq(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\tfilePath := filepath.Join(fullPath, f.Name())\n\tisFolder := f.IsDir() || isLinkedDir(f, filePath)\n\tvar size int64\n\tif !isFolder {\n\t\tsize = f.Size()\n\t}\n\tvar ctime time.Time\n\tt, err := times.Stat(filePath)\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,\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}\nfunc (d *Local) GetMeta(ctx context.Context, path string) (model.Obj, error) {\n\tf, err := os.Stat(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfile := d.FileInfoToObj(ctx, f, path, path)\n\t//h := \"123123\"\n\t//if s, ok := f.(model.SetHash); ok && file.GetHash() == (\"\",\"\")  {\n\t//\ts.SetHash(h,\"SHA1\")\n\t//}\n\treturn file, nil\n\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 strings.Contains(err.Error(), \"cannot find the file\") {\n\t\t\treturn nil, errs.ObjectNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\tisFolder := f.IsDir() || isLinkedDir(f, path)\n\tsize := f.Size()\n\tif isFolder {\n\t\tsize = 0\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\tvar link model.Link\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\tlink.MFile = open\n\t\t} else {\n\t\t\tlink.MFile = model.NewNopMFile(bytes.NewReader(buf.Bytes()))\n\t\t\t//link.Header.Set(\"Content-Length\", strconv.Itoa(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.MFile = open\n\t}\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\tif err := os.Rename(srcPath, dstPath); err != nil && strings.Contains(err.Error(), \"invalid cross-device link\") {\n\t\t// Handle cross-device file move in local driver\n\t\tif err = d.Copy(ctx, srcObj, dstDir); err != nil {\n\t\t\treturn err\n\t\t} else {\n\t\t\t// Directly remove file without check recycle bin if successfully copied\n\t\t\tif srcObj.IsDir() {\n\t\t\t\terr = os.RemoveAll(srcObj.GetPath())\n\t\t\t} else {\n\t\t\t\terr = os.Remove(srcObj.GetPath())\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\treturn err\n\t}\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\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\t// Copy using otiai10/copy to perform more secure & efficient copy\n\treturn cp.Copy(srcPath, dstPath, cp.Options{\n\t\tSync:          true, // Sync file to disk after copy, may have performance penalty in filesystem such as ZFS\n\t\tPreserveTimes: true,\n\t\tPreserveOwner: true,\n\t})\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\tdstPath := filepath.Join(d.RecycleBinPath, obj.GetName())\n\t\tif utils.Exists(dstPath) {\n\t\t\tdstPath = filepath.Join(d.RecycleBinPath, obj.GetName()+\"_\"+time.Now().Format(\"20060102150405\"))\n\t\t}\n\t\terr = os.Rename(obj.GetPath(), dstPath)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\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\treturn nil\n}\n\nvar _ driver.Driver = (*Local)(nil)\n"
  },
  {
    "path": "drivers/local/meta.go",
    "content": "package local\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\tThumbnail        bool   `json:\"thumbnail\" required:\"true\" help:\"enable thumbnail\"`\n\tUseFFmpeg        bool   `json:\"use_ffmpeg\" required:\"true\" help:\"use ffmpeg to generate 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\tThumbPixel       string `json:\"thumb_pixel\" default:\"320\" required:\"false\" help:\"Specifies the target width for image thumbnails in pixels. The height of the thumbnail will be calculated automatically to maintain the original aspect ratio of the image.\"`\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\tOnlyLocal:   true,\n\tLocalSort:   true,\n\tNoCache:     true,\n\tDefaultRoot: \"/\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Local{}\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\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/disintegration/imaging\"\n\tffmpeg \"github.com/u2takey/ffmpeg-go\"\n)\n\nfunc isLinkedDir(f fs.FileInfo, path string) bool {\n\tif f.Mode()&os.ModeSymlink == os.ModeSymlink || (runtime.GOOS == \"windows\" && f.Mode()&os.ModeIrregular != 0) {\n\t\tdst, err := os.Readlink(path)\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(filepath.Dir(path), dst)\n\t\t}\n\t\tdst, err = filepath.Abs(dst)\n\t\tif err != nil {\n\t\t\treturn false\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// resizeImageToBufferWithFFmpegGo 使用 ffmpeg-go 调整图片大小并输出到内存缓冲区\nfunc resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string /* e.g., \"image2pipe\", \"png_pipe\", \"mjpeg\" */) (*bytes.Buffer, error) {\n\toutBuffer := bytes.NewBuffer(nil)\n\n\t// Determine codec based on desired output format for piping\n\t// For generic image piping, 'image2' is often used with -f image2pipe\n\t// For specific formats to buffer, you might specify the codec directly\n\tvar vcodec string\n\tswitch outputFormat {\n\tcase \"png_pipe\": // if you want to ensure PNG format in buffer\n\t\tvcodec = \"png\"\n\tcase \"mjpeg\": // if you want to ensure JPEG format in buffer\n\t\tvcodec = \"mjpeg\"\n\t\t// default or \"image2pipe\" could leave codec choice more to ffmpeg or require -c:v later\n\t}\n\n\toutputArgs := ffmpeg.KwArgs{\n\t\t\"vf\":      fmt.Sprintf(\"scale=%d:-1:flags=lanczos,format=yuv444p\", width),\n\t\t\"vframes\": \"1\",\n\t\t\"f\":       outputFormat, // Format for piping (e.g., image2pipe, png_pipe)\n\t}\n\tif vcodec != \"\" {\n\t\toutputArgs[\"vcodec\"] = vcodec\n\t}\n\tif outputFormat == \"mjpeg\" {\n\t\toutputArgs[\"q:v\"] = \"3\"\n\t}\n\n\terr := ffmpeg.Input(inputFile).\n\t\tOutput(\"pipe:\", outputArgs). // Output to pipe (stdout)\n\t\tGlobalArgs(\"-loglevel\", \"error\").\n\t\tSilent(true).                     // Suppress ffmpeg's own console output\n\t\tWithOutput(outBuffer, os.Stderr). // Capture stdout to outBuffer, stderr to os.Stderr\n\t\t// ErrorToStdOut(). // Alternative: send ffmpeg's stderr to Go's stdout\n\t\tRun()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ffmpeg-go failed to resize image %s to buffer: %w\", inputFile, err)\n\t}\n\tif outBuffer == nil || outBuffer.Len() == 0 {\n\t\treturn nil, fmt.Errorf(\"ffmpeg-go produced empty buffer for %s\", inputFile)\n\t}\n\n\treturn outBuffer, nil\n}\n\nfunc generateThumbnailWithImagingOptimized(imagePath string, targetWidth int, quality int) (*bytes.Buffer, error) {\n\n\tfile, err := os.Open(imagePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open image: %w\", err)\n\t}\n\tdefer file.Close()\n\n\timg, err := imaging.Decode(file, imaging.AutoOrientation(true))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode image: %w\", err)\n\t}\n\n\tthumbImg := imaging.Resize(img, targetWidth, 0, imaging.Lanczos)\n\timg = nil\n\n\tvar buf bytes.Buffer\n\t// imaging.Encode\n\t// imaging.PNG, imaging.JPEG, imaging.GIF, imaging.BMP, imaging.TIFF\n\toutputFormat := imaging.JPEG\n\tencodeOptions := []imaging.EncodeOption{imaging.JPEGQuality(quality)}\n\n\t// outputFormat := imaging.PNG\n\t// encodeOptions := []imaging.EncodeOption{}\n\n\terr = imaging.Encode(&buf, thumbImg, outputFormat, encodeOptions...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encode thumbnail: %w\", err)\n\t}\n\n\tthumbImg = nil\n\n\treturn &buf, nil\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\", \"vf\": fmt.Sprintf(\"scale=%d:-1:flags=lanczos\", d.thumbPixel)}).\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 := \"alist_thumb_\"\n\tthumbName := thumbPrefix + utils.GetMD5EncodeStr(fmt.Sprintf(\"%s:%d\", fullPath, d.thumbSize)) + \".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\tif d.useFFmpeg {\n\t\t\timgData, err := resizeImageToBufferWithFFmpegGo(fullPath, d.thumbPixel, \"image2pipe\")\n\t\t\tsrcBuf = imgData\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t} else {\n\t\t\timgData, err := generateThumbnailWithImagingOptimized(fullPath, d.thumbPixel, 70)\n\t\t\tsrcBuf = imgData\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif d.ThumbCacheFolder != \"\" {\n\t\terr := os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), srcBuf.Bytes(), 0666)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\treturn srcBuf, nil, nil\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*/\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype Mediafire struct {\n\tmodel.Storage\n\tAddition\n\tcron *cron.Cron\n\n\tactionToken string\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\nfunc (d *Mediafire) Init(ctx context.Context) error {\n\tif d.SessionToken == \"\" {\n\t\treturn fmt.Errorf(\"Init :: [MediaFire] {critical} missing sessionToken\")\n\t}\n\n\tif d.Cookie == \"\" {\n\t\treturn fmt.Errorf(\"Init :: [MediaFire] {critical} missing Cookie\")\n\t}\n\n\tif _, err := d.getSessionToken(ctx); err != nil {\n\n\t\td.renewToken(ctx)\n\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\td.renewToken(ctx)\n\t\t})\n\n\t}\n\n\treturn nil\n}\n\nfunc (d *Mediafire) Drop(ctx context.Context) error {\n\treturn nil\n}\n\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\nfunc (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\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).Get(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\t//\"User-Agent\": []string{base.UserAgent},\n\t\t},\n\t}, nil\n}\n\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(\"/folder/create.php\", data, &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 API error: %s\", resp.Response.Result)\n\t}\n\n\tcreated, _ := time.Parse(\"2006-01-02T15:04:05Z\", resp.Response.CreatedUTC)\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       resp.Response.FolderKey,\n\t\t\tName:     resp.Response.Name,\n\t\t\tSize:     0,\n\t\t\tModified: created,\n\t\t\tCtime:    created,\n\t\t\tIsFolder: true,\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}, nil\n}\n\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(endpoint, data, &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 API error: %s\", resp.Response.Result)\n\t}\n\n\treturn srcObj, nil\n}\n\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(endpoint, data, &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 API error: %s\", resp.Response.Result)\n\t}\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       srcObj.GetID(),\n\t\t\tName:     newName,\n\t\t\tSize:     srcObj.GetSize(),\n\t\t\tModified: srcObj.ModTime(),\n\t\t\tCtime:    srcObj.CreateTime(),\n\t\t\tIsFolder: srcObj.IsDir(),\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}, nil\n}\n\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(endpoint, data, &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 API error: %s\", resp.Response.Result)\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.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:       newID,\n\t\t\tName:     srcObj.GetName(),\n\t\t\tSize:     srcObj.GetSize(),\n\t\t\tModified: srcObj.ModTime(),\n\t\t\tCtime:    srcObj.CreateTime(),\n\t\t\tIsFolder: srcObj.IsDir(),\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}, nil\n}\n\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(endpoint, 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 API error: %s\", resp.Response.Result)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\t_, err := d.PutResult(ctx, dstDir, file, up)\n\treturn err\n}\n\nfunc (d *Mediafire) PutResult(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\n\ttempFile, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tempFile.Close()\n\n\tosFile, ok := tempFile.(*os.File)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expected *os.File, got %T\", tempFile)\n\t}\n\n\tfileHash, err := d.calculateSHA256(osFile)\n\tif err != nil {\n\t\treturn nil, err\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.ResumableUpload.AllUnitsReady == \"yes\" {\n\t\tup(100.0)\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 {\n\t\t\treturn existingFile, nil\n\t\t}\n\t}\n\n\tvar pollKey string\n\n\tif checkResp.Response.ResumableUpload.AllUnitsReady != \"yes\" {\n\n\t\tvar err error\n\n\t\tpollKey, err = d.uploadUnits(ctx, osFile, 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\n\t\tpollKey = checkResp.Response.ResumableUpload.UploadKey\n\t}\n\n\t//fmt.Printf(\"pollKey: %+v\\n\", pollKey)\n\n\tpollResp, err := d.pollUpload(ctx, pollKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquickKey := pollResp.Response.Doupload.QuickKey\n\n\treturn &model.ObjThumb{\n\t\tObject: model.Object{\n\t\t\tID:   quickKey,\n\t\t\tName: file.GetName(),\n\t\t\tSize: file.GetSize(),\n\t\t},\n\t\tThumbnail: model.Thumbnail{},\n\t}, nil\n}\n\nfunc (d *Mediafire) 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 *Mediafire) 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 *Mediafire) 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 *Mediafire) 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 *Mediafire) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\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*/\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\t//driver.RootID\n\n\tSessionToken string `json:\"session_token\" required:\"true\" type:\"string\" help:\"Required for MediaFire API\"`\n\tCookie       string `json:\"cookie\" required:\"true\" type:\"string\" help:\"Required for navigation\"`\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}\n\nvar config = driver.Config{\n\tName:              \"MediaFire\",\n\tLocalSort:         false,\n\tOnlyLocal:         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\tsecChUa:         \"\\\"Not)A;Brand\\\";v=\\\"8\\\", \\\"Chromium\\\";v=\\\"139\\\", \\\"Google Chrome\\\";v=\\\"139\\\"\",\n\t\t\tsecChUaPlatform: \"Windows\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n\t\t}\n\t})\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"
  },
  {
    "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*/\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\nfunc (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {\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, deflate, br, zstd\")\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\tbody, err := io.ReadAll(resp.Body)\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\nfunc (d *Mediafire) renewToken(_ 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(\"/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\tfiles := make([]File, 0)\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\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\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(_ 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(\"/folder/get_content.php\", data, &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 API error: %s\", resp.Response.Result)\n\t}\n\n\treturn &resp, nil\n}\n\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) getForm(endpoint string, query map[string]string, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\n\treq.SetQueryParams(query)\n\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\": d.Cookie,\n\t\t//\"User-Agent\": base.UserAgent,\n\t\t\"User-Agent\": d.userAgent,\n\t\t\"Origin\":     d.appBase,\n\t\t\"Referer\":    d.appBase + \"/\",\n\t})\n\n\t// If response OK\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\t// Targets MediaFire API\n\tres, err := req.Get(d.apiBase + endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn res.Body(), nil\n}\n\nfunc (d *Mediafire) postForm(endpoint string, data map[string]string, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\n\treq.SetFormData(data)\n\n\treq.SetHeaders(map[string]string{\n\t\t\"Cookie\":       d.Cookie,\n\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t//\"User-Agent\": base.UserAgent,\n\t\t\"User-Agent\": d.userAgent,\n\t\t\"Origin\":     d.appBase,\n\t\t\"Referer\":    d.appBase + \"/\",\n\t})\n\n\t// If response OK\n\tif resp != nil {\n\t\treq.SetResult(resp)\n\t}\n\n\t// Targets MediaFire API\n\tres, err := req.Post(d.apiBase + endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn res.Body(), nil\n}\n\nfunc (d *Mediafire) getDirectDownloadLink(_ 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(\"/file/get_links.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 API error: %s\", resp.Response.Result)\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) calculateSHA256(file *os.File) (string, error) {\n\thasher := sha256.New()\n\tif _, err := file.Seek(0, 0); err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err := io.Copy(hasher, file); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn hex.EncodeToString(hasher.Sum(nil)), nil\n}\n\nfunc (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {\n\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(\"/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 resp.Response.Result != \"Success\" {\n\t\treturn nil, fmt.Errorf(\"MediaFire upload check failed: %s\", resp.Response.Result)\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Mediafire) resumableUpload(ctx context.Context, folderKey, uploadKey string, unitData []byte, unitID int, fileHash, filename string, totalFileSize int64) (string, error) {\n\tactionToken, err := d.getActionToken(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl := d.apiBase + \"/upload/resumable.php\"\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(unitData))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tq := req.URL.Query()\n\tq.Add(\"folder_key\", folderKey)\n\tq.Add(\"response_format\", \"json\")\n\tq.Add(\"session_token\", actionToken)\n\tq.Add(\"key\", uploadKey)\n\treq.URL.RawQuery = q.Encode()\n\n\treq.Header.Set(\"x-filehash\", fileHash)\n\treq.Header.Set(\"x-filesize\", strconv.FormatInt(totalFileSize, 10))\n\treq.Header.Set(\"x-unit-id\", strconv.Itoa(unitID))\n\treq.Header.Set(\"x-unit-size\", strconv.FormatInt(int64(len(unitData)), 10))\n\treq.Header.Set(\"x-unit-hash\", d.sha256Hex(bytes.NewReader(unitData)))\n\treq.Header.Set(\"x-filename\", filename)\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.ContentLength = int64(len(unitData))\n\n\t/* fmt.Printf(\"Debug resumable upload request:\\n\")\n\tfmt.Printf(\"  URL: %s\\n\", req.URL.String())\n\tfmt.Printf(\"  Headers: %+v\\n\", req.Header)\n\tfmt.Printf(\"  Unit ID: %d\\n\", unitID)\n\tfmt.Printf(\"  Unit Size: %d\\n\", len(unitData))\n\tfmt.Printf(\"  Upload Key: %s\\n\", uploadKey)\n\tfmt.Printf(\"  Action Token: %s\\n\", actionToken) */\n\n\tres, err := base.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response body: %v\", err)\n\t}\n\n\t//fmt.Printf(\"MediaFire resumable upload response (status %d): %s\\n\", res.StatusCode, string(body))\n\n\tvar uploadResp struct {\n\t\tResponse struct {\n\t\t\tDoupload struct {\n\t\t\t\tKey string `json:\"key\"`\n\t\t\t} `json:\"doupload\"`\n\t\t\tResult string `json:\"result\"`\n\t\t} `json:\"response\"`\n\t}\n\n\tif err := json.Unmarshal(body, &uploadResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse response: %v\", err)\n\t}\n\n\tif res.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"resumable upload failed with status %d\", res.StatusCode)\n\t}\n\n\treturn uploadResp.Response.Doupload.Key, nil\n}\n\nfunc (d *Mediafire) uploadUnits(ctx context.Context, file *os.File, 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, len(stringWords))\n\tfor i, word := range stringWords {\n\t\tintWords[i], _ = strconv.Atoi(word)\n\t}\n\n\tvar finalUploadKey string\n\n\tfor unitID := 0; unitID < numUnits; unitID++ {\n\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn \"\", ctx.Err()\n\t\t}\n\n\t\tif d.isUnitUploaded(intWords, unitID) {\n\t\t\tup(float64(unitID+1) * 100 / float64(numUnits))\n\t\t\tcontinue\n\t\t}\n\n\t\tuploadKey, err := d.uploadSingleUnit(ctx, file, unitID, unitSize, fileHash, filename, uploadKey, folderKey)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfinalUploadKey = uploadKey\n\n\t\tup(float64(unitID+1) * 100 / float64(numUnits))\n\t}\n\n\treturn finalUploadKey, nil\n}\n\nfunc (d *Mediafire) uploadSingleUnit(ctx context.Context, file *os.File, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string) (string, error) {\n\tstart := int64(unitID) * unitSize\n\tsize := unitSize\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfileSize := stat.Size()\n\n\tif start+size > fileSize {\n\t\tsize = fileSize - start\n\t}\n\n\tunitData := make([]byte, size)\n\tif _, err := file.ReadAt(unitData, start); 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(_ context.Context) (string, error) {\n\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(\"/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\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(\"/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 resp.Response.Result != \"Success\" {\n\t\treturn nil, fmt.Errorf(\"MediaFire poll upload failed: %s\", resp.Response.Result)\n\t}\n\n\treturn &resp, nil\n}\n\nfunc (d *Mediafire) sha256Hex(r io.Reader) string {\n\th := sha256.New()\n\tio.Copy(h, r)\n\treturn hex.EncodeToString(h.Sum(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\n\tif fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {\n\t\treturn fileInfo, nil\n\t}\n\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(_ 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(\"/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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = tempFile.Close()\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tDeviceFingerprint string `json:\"device_fingerprint\" required:\"true\"`\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/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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 d.DeviceFingerprint != \"\" {\n\t\treq.SetHeader(\"X-Device-Fingerprint\", d.DeviceFingerprint)\n\t}\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/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/rclone/rclone/lib/readers\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 = -1\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\tresultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader}\n\t\tresultLink := &model.Link{\n\t\t\tRangeReadCloser: resultRangeReadCloser,\n\t\t}\n\t\treturn resultLink, 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, false)\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\n\nvar config = driver.Config{\n\tName:      \"Mega_nz\",\n\tLocalSort: true,\n\tOnlyLocal: 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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/t3rm1n4l/go-mega\"\n\t\"io\"\n\t\"sync\"\n\t\"time\"\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 dir.GetID() == \"\" {\n\t\treturn nil\n\t}\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 dir.GetPath() != \"/\" {\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\", \"POST\", 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 dir.GetPath() != \"/\" {\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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", 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\", \"POST\", setBody(map[string]string{\"folderId\": obj.GetID()}), nil)\n\t\treturn err\n\t} else {\n\t\terr := d.request(\"/files/delete\", \"POST\", 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\treq := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetFileReader(\"file\", stream.GetName(), reader).\n\t\tSetFormData(map[string]string{\n\t\t\t\"folderId\":    handleFolderId(dstDir).(string),\n\t\t\t\"name\":        stream.GetName(),\n\t\t\t\"comment\":     \"\",\n\t\t\t\"isSensitive\": \"false\",\n\t\t\t\"force\":       \"false\",\n\t\t}).\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\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/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.CacheFullInTempFile()\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\t\tsem := semaphore.NewWeighted(3)\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\tif err = sem.Acquire(ctx, 1); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer sem.Release(1)\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()) / float64(len(parts)))\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\t// DefaultRoot: \"root, / or other\",\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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 (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {\n\tif path == \"/\" {\n\t\treturn &model.Object{\n\t\t\tIsFolder: true,\n\t\t\tPath:     path,\n\t\t}, nil\n\t}\n\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\" {\n\t\t\treturn lrc.getLyricLink(), nil\n\t\t} else {\n\t\t\treturn lrc.getProxyLink(args), 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/alist-org/alist/v3/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(args model.LinkArgs) *model.Link {\n\trawURL := common.GetApiUrl(args.HttpReq) + \"/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\treader := strings.NewReader(lrc.lyric)\n\treturn &model.Link{\n\t\tRangeReadCloser: &model.RangeReadCloser{\n\t\t\tRangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\t\tif httpRange.Length < 0 {\n\t\t\t\t\treturn io.NopCloser(reader), nil\n\t\t\t\t}\n\t\t\t\tsr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)\n\t\t\t\treturn io.NopCloser(sr), nil\n\t\t\t},\n\t\t},\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\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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.CacheFullInTempFile()\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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}\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\treturn d.refreshToken()\n}\n\nfunc (d *Onedrive) Drop(ctx context.Context) error {\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\treturn fileToObj(src, dir.GetID()), 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\nvar _ driver.Driver = (*Onedrive)(nil)\n"
  },
  {
    "path": "drivers/onedrive/meta.go",
    "content": "package onedrive\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tClientID     string `json:\"client_id\" required:\"true\"`\n\tClientSecret string `json:\"client_secret\" required:\"true\"`\n\tRedirectUri  string `json:\"redirect_uri\" required:\"true\" default:\"https://alistgo.com/tool/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}\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/alist-org/alist/v3/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"
  },
  {
    "path": "drivers/onedrive/util.go",
    "content": "package onedrive\n\nimport (\n\t\"bytes\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\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{}) ([]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\" {\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]interface{}{\"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\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tvar finish int64 = 0\n\tDEFAULT := d.ChunkSize * 1024 * 1024\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"PUT\", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\tswitch {\n\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\tres.Body.Close()\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error %d\", maxRetries, res.StatusCode)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[Onedrive] server errors %d while uploading, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\treturn errors.New(string(data))\n\t\tdefault:\n\t\t\tres.Body.Close()\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\t}\n\t}\n\treturn 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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\treturn fileToObj(src, dir.GetID()), 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\nvar _ driver.Driver = (*OnedriveAPP)(nil)\n"
  },
  {
    "path": "drivers/onedrive_app/meta.go",
    "content": "package onedrive_app\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\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/alist-org/alist/v3/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"
  },
  {
    "path": "drivers/onedrive_app/util.go",
    "content": "package onedrive_app\n\nimport (\n\t\"bytes\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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{}) ([]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\" {\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\tuploadUrl := jsoniter.Get(res, \"uploadUrl\").ToString()\n\tvar finish int64 = 0\n\tDEFAULT := d.ChunkSize * 1024 * 1024\n\tretryCount := 0\n\tmaxRetries := 3\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\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\treq, err := http.NewRequest(\"PUT\", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq = req.WithContext(ctx)\n\t\treq.ContentLength = byteSize\n\t\t// req.Header.Set(\"Content-Length\", strconv.Itoa(int(byteSize)))\n\t\treq.Header.Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", finish, finish+byteSize-1, stream.GetSize()))\n\t\tres, err := base.HttpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t\tswitch {\n\t\tcase res.StatusCode >= 500 && res.StatusCode <= 504:\n\t\t\tretryCount++\n\t\t\tif retryCount > maxRetries {\n\t\t\t\tres.Body.Close()\n\t\t\t\treturn fmt.Errorf(\"upload failed after %d retries due to server errors, error %d\", maxRetries, res.StatusCode)\n\t\t\t}\n\t\t\tbackoff := time.Duration(1<<retryCount) * time.Second\n\t\t\tutils.Log.Warnf(\"[OnedriveAPP] server errors %d while uploading, retrying after %v...\", res.StatusCode, backoff)\n\t\t\ttime.Sleep(backoff)\n\t\tcase res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:\n\t\t\tdata, _ := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\treturn errors.New(string(data))\n\t\tdefault:\n\t\t\tres.Body.Close()\n\t\t\tretryCount = 0\n\t\t\tfinish += byteSize\n\t\t\tup(float64(finish) * 100 / float64(stream.GetSize()))\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/onedrive_sharelink/driver.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype OnedriveSharelink struct {\n\tmodel.Storage\n\tcron *cron.Cron\n\tAddition\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\td.Headers, err = d.getHeaders()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"%+v\", err)\n\t\t}\n\t})\n\n\t// Get initial headers\n\td.Headers, err = d.getHeaders()\n\tif err != nil {\n\t\treturn err\n\t}\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\tpath := dir.GetPath()\n\tfiles, err := d.getFiles(path)\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\treturn fileToObj(src), 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\theader := d.Headers\n\n\t// If the headers are older than 30 minutes, get new headers\n\tif d.HeaderTime < time.Now().Unix()-1800 {\n\t\tvar err error\n\t\tlog.Debug(\"headers are older than 30 minutes, get new headers\")\n\t\theader, err = d.getHeaders()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &model.Link{\n\t\tURL:    url,\n\t\tHeader: header,\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"
  },
  {
    "path": "drivers/onedrive_sharelink/meta.go",
    "content": "package onedrive_sharelink\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tCheckStatus: false,\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/alist-org/alist/v3/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\"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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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() (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.NewRequest(\"GET\", 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(path string) ([]Item, error) {\n\tclientNoDirect := NewNoRedirectCLient()\n\treq, err := http.NewRequest(\"GET\", 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()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.getFiles(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(\"POST\", 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()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn d.getFiles(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(\"POST\", 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()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn d.getFiles(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(\"POST\", 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()\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(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/pcloud/driver.go",
    "content": "package pcloud\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype PCloud struct {\n\tmodel.Storage\n\tAddition\n\tAccessToken string // Actual access token obtained from refresh token\n}\n\nfunc (d *PCloud) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *PCloud) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *PCloud) Init(ctx context.Context) error {\n\t// Map hostname selection to actual API endpoints\n\tif d.Hostname == \"us\" {\n\t\td.Hostname = \"api.pcloud.com\"\n\t} else if d.Hostname == \"eu\" {\n\t\td.Hostname = \"eapi.pcloud.com\"\n\t}\n\n\t// Set default root folder ID if not provided\n\tif d.RootFolderID == \"\" {\n\t\td.RootFolderID = \"d0\"\n\t}\n\n\t// Use the access token directly (like rclone)\n\td.AccessToken = d.RefreshToken // RefreshToken field actually contains the access_token\n\treturn nil\n}\n\nfunc (d *PCloud) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *PCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfolderID := d.RootFolderID\n\tif dir.GetID() != \"\" {\n\t\tfolderID = dir.GetID()\n\t}\n\n\tfiles, err := d.getFiles(folderID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.SliceConvert(files, func(src FileObject) (model.Obj, error) {\n\t\treturn fileToObj(src), nil\n\t})\n}\n\nfunc (d *PCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tdownloadURL, err := d.getDownloadLink(file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Link{\n\t\tURL: downloadURL,\n\t}, nil\n}\n\n// Mkdir implements driver.Mkdir\nfunc (d *PCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tparentID := d.RootFolderID\n\tif parentDir.GetID() != \"\" {\n\t\tparentID = parentDir.GetID()\n\t}\n\n\treturn d.createFolder(parentID, dirName)\n}\n\n// Move implements driver.Move\nfunc (d *PCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// pCloud uses renamefile/renamefolder for both rename and move\n\tendpoint := \"/renamefile\"\n\tparamName := \"fileid\"\n\n\tif srcObj.IsDir() {\n\t\tendpoint = \"/renamefolder\"\n\t\tparamName = \"folderid\"\n\t}\n\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(endpoint, \"POST\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\tparamName:      extractID(srcObj.GetID()),\n\t\t\t\"tofolderid\":   extractID(dstDir.GetID()),\n\t\t\t\"toname\":       srcObj.GetName(),\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}\n\n// Rename implements driver.Rename\nfunc (d *PCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tendpoint := \"/renamefile\"\n\tparamName := \"fileid\"\n\n\tif srcObj.IsDir() {\n\t\tendpoint = \"/renamefolder\"\n\t\tparamName = \"folderid\"\n\t}\n\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(endpoint, \"POST\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\tparamName: extractID(srcObj.GetID()),\n\t\t\t\"toname\":  newName,\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}\n\n// Copy implements driver.Copy\nfunc (d *PCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tendpoint := \"/copyfile\"\n\tparamName := \"fileid\"\n\n\tif srcObj.IsDir() {\n\t\tendpoint = \"/copyfolder\"\n\t\tparamName = \"folderid\"\n\t}\n\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(endpoint, \"POST\", func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\tparamName:    extractID(srcObj.GetID()),\n\t\t\t\"tofolderid\": extractID(dstDir.GetID()),\n\t\t\t\"toname\":     srcObj.GetName(),\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}\n\n// Remove implements driver.Remove\nfunc (d *PCloud) Remove(ctx context.Context, obj model.Obj) error {\n\treturn d.delete(obj.GetID(), obj.IsDir())\n}\n\n// Put implements driver.Put\nfunc (d *PCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tparentID := d.RootFolderID\n\tif dstDir.GetID() != \"\" {\n\t\tparentID = dstDir.GetID()\n\t}\n\n\treturn d.uploadFile(ctx, stream, parentID, stream.GetName(), stream.GetSize())\n}"
  },
  {
    "path": "drivers/pcloud/meta.go",
    "content": "package pcloud\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Using json tag \"access_token\" for UI display, but internally it's a refresh token\n\tRefreshToken   string `json:\"access_token\" required:\"true\" help:\"OAuth token from pCloud authorization\"`\n\tHostname       string `json:\"hostname\" type:\"select\" options:\"us,eu\" default:\"us\" help:\"Select pCloud server region\"`\n\tRootFolderID   string `json:\"root_folder_id\" help:\"Get folder ID from URL like https://my.pcloud.com/#/filemanager?folder=12345678901 (leave empty for root folder)\"`\n\tClientID       string `json:\"client_id\" help:\"Custom OAuth client ID (optional)\"`\n\tClientSecret   string `json:\"client_secret\" help:\"Custom OAuth client secret (optional)\"`\n}\n\n// Implement IRootId interface\nfunc (a Addition) GetRootId() string {\n\treturn a.RootFolderID\n}\n\nvar config = driver.Config{\n\tName:        \"pCloud\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &PCloud{}\n\t})\n}"
  },
  {
    "path": "drivers/pcloud/types.go",
    "content": "package pcloud\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\n// ErrorResult represents a pCloud API error response\ntype ErrorResult struct {\n\tResult int    `json:\"result\"`\n\tError  string `json:\"error\"`\n}\n\n// TokenResponse represents OAuth token response\ntype TokenResponse struct {\n\tAccessToken string `json:\"access_token\"`\n\tTokenType   string `json:\"token_type\"`\n}\n\n// ItemResult represents a common pCloud API response\ntype ItemResult struct {\n\tResult   int          `json:\"result\"`\n\tMetadata *FolderMeta  `json:\"metadata,omitempty\"`\n}\n\n// FolderMeta contains folder metadata including contents\ntype FolderMeta struct {\n\tContents []FileObject `json:\"contents,omitempty\"`\n}\n\n// DownloadLinkResult represents download link response\ntype DownloadLinkResult struct {\n\tResult int      `json:\"result\"`\n\tHosts  []string `json:\"hosts\"`\n\tPath   string   `json:\"path\"`\n}\n\n// FileObject represents a file or folder object in pCloud\ntype FileObject struct {\n\tName       string    `json:\"name\"`\n\tCreated    string    `json:\"created\"`    // pCloud returns RFC1123 format string\n\tModified   string    `json:\"modified\"`   // pCloud returns RFC1123 format string\n\tIsFolder   bool      `json:\"isfolder\"`\n\tFolderID   uint64    `json:\"folderid,omitempty\"`\n\tFileID     uint64    `json:\"fileid,omitempty\"`\n\tSize       uint64    `json:\"size\"`\n\tParentID   uint64    `json:\"parentfolderid\"`\n\tIcon       string    `json:\"icon,omitempty\"`\n\tHash       uint64    `json:\"hash,omitempty\"`\n\tCategory   int       `json:\"category,omitempty\"`\n\tID         string    `json:\"id,omitempty\"`\n}\n\n// Convert FileObject to model.Obj\nfunc fileToObj(f FileObject) model.Obj {\n\t// Parse RFC1123 format time from pCloud\n\tmodTime, _ := time.Parse(time.RFC1123, f.Modified)\n\n\tobj := model.Object{\n\t\tName:     f.Name,\n\t\tSize:     int64(f.Size),\n\t\tModified: modTime,\n\t\tIsFolder: f.IsFolder,\n\t}\n\n\tif f.IsFolder {\n\t\tobj.ID = \"d\" + strconv.FormatUint(f.FolderID, 10)\n\t} else {\n\t\tobj.ID = \"f\" + strconv.FormatUint(f.FileID, 10)\n\t}\n\n\treturn &obj\n}\n\n// Extract numeric ID from string ID (remove 'd' or 'f' prefix)\nfunc extractID(id string) string {\n\tif len(id) > 1 && (id[0] == 'd' || id[0] == 'f') {\n\t\treturn id[1:]\n\t}\n\treturn id\n}\n\n// Get folder ID from path, return \"0\" for root\nfunc getFolderID(path string) string {\n\tif path == \"/\" || path == \"\" {\n\t\treturn \"0\"\n\t}\n\treturn extractID(path)\n}"
  },
  {
    "path": "drivers/pcloud/util.go",
    "content": "package pcloud\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\tdefaultClientID     = \"DnONSzyJXpm\"\n\tdefaultClientSecret = \"VKEnd3ze4jsKFGg8TJiznwFG8\"\n)\n\n// Get API base URL\nfunc (d *PCloud) getAPIURL() string {\n\treturn \"https://\" + d.Hostname\n}\n\n// Get OAuth client credentials\nfunc (d *PCloud) getClientCredentials() (string, string) {\n\tclientID := d.ClientID\n\tclientSecret := d.ClientSecret\n\n\tif clientID == \"\" {\n\t\tclientID = defaultClientID\n\t}\n\tif clientSecret == \"\" {\n\t\tclientSecret = defaultClientSecret\n\t}\n\n\treturn clientID, clientSecret\n}\n\n// Refresh OAuth access token\nfunc (d *PCloud) refreshToken() error {\n\tclientID, clientSecret := d.getClientCredentials()\n\n\tvar resp TokenResponse\n\t_, err := base.RestyClient.R().\n\t\tSetFormData(map[string]string{\n\t\t\t\"client_id\":     clientID,\n\t\t\t\"client_secret\": clientSecret,\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": d.RefreshToken,\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(d.getAPIURL() + \"/oauth2_token\")\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\td.AccessToken = resp.AccessToken\n\treturn nil\n}\n\n// shouldRetry determines if an error should be retried based on pCloud-specific logic\nfunc (d *PCloud) shouldRetry(statusCode int, apiError *ErrorResult) bool {\n\t// HTTP-level retry conditions\n\tif statusCode == 429 || statusCode >= 500 {\n\t\treturn true\n\t}\n\n\t// pCloud API-specific retry conditions (like rclone)\n\tif apiError != nil && apiError.Result != 0 {\n\t\t// 4xxx: rate limiting\n\t\tif apiError.Result/1000 == 4 {\n\t\t\treturn true\n\t\t}\n\t\t// 5xxx: internal errors\n\t\tif apiError.Result/1000 == 5 {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// requestWithRetry makes authenticated API request with retry logic\nfunc (d *PCloud) requestWithRetry(endpoint string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\tmaxRetries := 3\n\tbaseDelay := 500 * time.Millisecond\n\n\tfor attempt := 0; attempt <= maxRetries; attempt++ {\n\t\tbody, err := d.request(endpoint, method, callback, resp)\n\t\tif err == nil {\n\t\t\treturn body, nil\n\t\t}\n\n\t\t// If this is the last attempt, return the error\n\t\tif attempt == maxRetries {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Check if we should retry based on error type\n\t\tif !d.shouldRetryError(err) {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Exponential backoff\n\t\tdelay := baseDelay * time.Duration(1<<attempt)\n\t\ttime.Sleep(delay)\n\t}\n\n\treturn nil, fmt.Errorf(\"max retries exceeded\")\n}\n\n// shouldRetryError checks if an error should trigger a retry\nfunc (d *PCloud) shouldRetryError(err error) bool {\n\t// For now, we'll retry on any error\n\t// In production, you'd want more specific error handling\n\treturn true\n}\n\n// Make authenticated API request\nfunc (d *PCloud) request(endpoint string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\n\t// Add access token as query parameter (pCloud doesn't use Bearer auth)\n\treq.SetQueryParam(\"access_token\", d.AccessToken)\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\tvar res *resty.Response\n\tvar err error\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\tres, err = req.Get(d.getAPIURL() + endpoint)\n\tcase http.MethodPost:\n\t\tres, err = req.Post(d.getAPIURL() + endpoint)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported method: %s\", method)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Check for API errors with pCloud-specific logic\n\tif res.StatusCode() != 200 {\n\t\tvar errResp ErrorResult\n\t\tif err := utils.Json.Unmarshal(res.Body(), &errResp); err == nil {\n\t\t\t// Check if this error should trigger a retry\n\t\t\tif d.shouldRetry(res.StatusCode(), &errResp) {\n\t\t\t\treturn nil, fmt.Errorf(\"pCloud API error (retryable): %s (result: %d)\", errResp.Error, errResp.Result)\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"pCloud API error: %s (result: %d)\", errResp.Error, errResp.Result)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"HTTP error: %d\", res.StatusCode())\n\t}\n\n\treturn res.Body(), nil\n}\n\n// List files in a folder\nfunc (d *PCloud) getFiles(folderID string) ([]FileObject, error) {\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(\"/listfolder\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"folderid\", extractID(folderID))\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn nil, fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\tif resp.Metadata == nil {\n\t\treturn []FileObject{}, nil\n\t}\n\n\treturn resp.Metadata.Contents, nil\n}\n\n// Get download link for a file\nfunc (d *PCloud) getDownloadLink(fileID string) (string, error) {\n\tvar resp DownloadLinkResult\n\t_, err := d.requestWithRetry(\"/getfilelink\", http.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"fileid\", extractID(fileID))\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn \"\", fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\tif len(resp.Hosts) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no download hosts available\")\n\t}\n\n\treturn \"https://\" + resp.Hosts[0] + resp.Path, nil\n}\n\n// Create a folder\nfunc (d *PCloud) createFolder(parentID, name string) error {\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(\"/createfolder\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"folderid\": extractID(parentID),\n\t\t\t\"name\":     name,\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}\n\n// Delete a file or folder\nfunc (d *PCloud) delete(objID string, isFolder bool) error {\n\tendpoint := \"/deletefile\"\n\tparamName := \"fileid\"\n\n\tif isFolder {\n\t\tendpoint = \"/deletefolderrecursive\"\n\t\tparamName = \"folderid\"\n\t}\n\n\tvar resp ItemResult\n\t_, err := d.requestWithRetry(endpoint, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\tparamName: extractID(objID),\n\t\t})\n\t}, &resp)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}\n\n// Upload a file using direct /uploadfile endpoint like rclone\nfunc (d *PCloud) uploadFile(ctx context.Context, file io.Reader, parentID, name string, size int64) error {\n\t// pCloud requires Content-Length, so we need to know the size\n\tif size <= 0 {\n\t\treturn fmt.Errorf(\"file size must be provided for pCloud upload\")\n\t}\n\n\t// Upload directly to /uploadfile endpoint like rclone\n\tvar resp ItemResult\n\treq := base.RestyClient.R().\n\t\tSetQueryParam(\"access_token\", d.AccessToken).\n\t\tSetHeader(\"Content-Length\", strconv.FormatInt(size, 10)).\n\t\tSetFileReader(\"content\", name, file).\n\t\tSetFormData(map[string]string{\n\t\t\t\"filename\": name,\n\t\t\t\"folderid\": extractID(parentID),\n\t\t\t\"nopartial\": \"1\",\n\t\t})\n\n\t// Use PUT method like rclone\n\tres, err := req.Put(d.getAPIURL() + \"/uploadfile\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Parse response\n\tif err := utils.Json.Unmarshal(res.Body(), &resp); err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Result != 0 {\n\t\treturn fmt.Errorf(\"pCloud upload error: result code %d\", resp.Result)\n\t}\n\n\treturn nil\n}"
  },
  {
    "path": "drivers/pikpak/driver.go",
    "content": "package pikpak\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/pkg/utils/hash\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\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\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.SetQueryParams(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.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.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.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.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.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\thi := stream.GetHash()\n\tsha1Str := hi.GetHash(hash_extend.GCID)\n\tif len(sha1Str) < hash_extend.GCID.Width {\n\t\ttFile, err := stream.CacheFullInTempFile()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsha1Str, err = utils.HashFile(hash_extend.GCID, tFile, 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\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.SetBody(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/*\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tDefaultRoot: \"\",\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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 := oss.New(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\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\terr       error\n\t)\n\n\ttmpF, err := s.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ossClient, err = oss.New(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\"github.com/alist-org/alist/v3/internal/op\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tDefaultRoot: \"\",\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/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/ProtonMail/gopenpgp/v2/crypto\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\tcredentials *common.ProtonDriveCredential\n\n\tapiBase    string\n\tappVersion string\n\tprotonJson string\n\tuserAgent  string\n\tsdkVersion string\n\twebDriveAV string\n\n\ttempServer     *http.Server\n\ttempServerPort int\n\tdownloadTokens map[string]*downloadInfo\n\ttokenMutex     sync.RWMutex\n\n\tc *proton.Client\n\t//m *proton.Manager\n\n\tcredentialCacheFile string\n\n\t//userKR   *crypto.KeyRing\n\taddrKRs  map[string]*crypto.KeyRing\n\taddrData map[string]proton.Address\n\n\tMainShare *proton.Share\n\tRootLink  *proton.Link\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) error {\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Printf(\"ProtonDrive initialization panic: %v\", r)\n\t\t}\n\t}()\n\n\tif d.Username == \"\" {\n\t\treturn fmt.Errorf(\"username is required\")\n\t}\n\tif d.Password == \"\" {\n\t\treturn fmt.Errorf(\"password is required\")\n\t}\n\n\t//fmt.Printf(\"ProtonDrive Init: Username=%s, TwoFACode=%s\", d.Username, d.TwoFACode)\n\n\tif ctx == nil {\n\t\treturn fmt.Errorf(\"context cannot be nil\")\n\t}\n\n\tcachedCredentials, err := d.loadCachedCredentials()\n\tuseReusableLogin := false\n\tvar reusableCredential *common.ReusableCredentialData\n\n\tif err == nil && cachedCredentials != nil &&\n\t\tcachedCredentials.UID != \"\" && cachedCredentials.AccessToken != \"\" &&\n\t\tcachedCredentials.RefreshToken != \"\" && cachedCredentials.SaltedKeyPass != \"\" {\n\t\tuseReusableLogin = true\n\t\treusableCredential = cachedCredentials\n\t} else {\n\t\tuseReusableLogin = false\n\t\treusableCredential = &common.ReusableCredentialData{}\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.Username,\n\t\t\tPassword: d.Password,\n\t\t\tTwoFA:    d.TwoFACode,\n\t\t},\n\t\tEnableCaching:              true,\n\t\tConcurrentBlockUploadCount: 5,\n\t\tConcurrentFileCryptoCount:  2,\n\t\tUseReusableLogin:           false,\n\t\tReplaceExistingDraft:       true,\n\t\tReusableCredential:         reusableCredential,\n\t\tCredentialCacheFile:        d.credentialCacheFile,\n\t}\n\n\tif config.FirstLoginCredential == nil {\n\t\treturn fmt.Errorf(\"failed to create login credentials, FirstLoginCredential cannot be nil\")\n\t}\n\n\t//fmt.Printf(\"Calling NewProtonDrive...\")\n\n\tprotonDrive, credentials, err := proton_api_bridge.NewProtonDrive(\n\t\tctx,\n\t\tconfig,\n\t\tfunc(auth proton.Auth) {},\n\t\tfunc() {},\n\t)\n\n\tif credentials == nil && !useReusableLogin {\n\t\treturn fmt.Errorf(\"failed to get credentials from NewProtonDrive\")\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize ProtonDrive: %w\", err)\n\t}\n\n\td.protonDrive = protonDrive\n\n\tvar finalCredentials *common.ProtonDriveCredential\n\n\tif useReusableLogin {\n\n\t\t// For reusable login, create credentials from cached data\n\t\tfinalCredentials = &common.ProtonDriveCredential{\n\t\t\tUID:           reusableCredential.UID,\n\t\t\tAccessToken:   reusableCredential.AccessToken,\n\t\t\tRefreshToken:  reusableCredential.RefreshToken,\n\t\t\tSaltedKeyPass: reusableCredential.SaltedKeyPass,\n\t\t}\n\n\t\td.credentials = finalCredentials\n\t} else {\n\t\td.credentials = credentials\n\t}\n\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.credentials.UID, d.credentials.AccessToken, d.credentials.RefreshToken)\n\n\tsaltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.credentials.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.MainShare = protonDrive.MainShare\n\td.RootLink = protonDrive.RootLink\n\td.MainShareKR = protonDrive.MainShareKR\n\td.DefaultAddrKR = protonDrive.DefaultAddrKR\n\td.addrKRs = addrKRs\n\td.addrData = addrs\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) Drop(ctx context.Context) error {\n\tif d.tempServer != nil {\n\t\td.tempServer.Shutdown(ctx)\n\t}\n\treturn nil\n}\n\nfunc (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar linkID string\n\n\tif dir.GetPath() == \"/\" {\n\t\tlinkID = d.protonDrive.RootLink.LinkID\n\t} else {\n\n\t\tlink, err := d.searchByPath(ctx, dir.GetPath(), true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlinkID = link.LinkID\n\t}\n\n\tentries, err := d.protonDrive.ListDirectory(ctx, linkID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list directory: %w\", err)\n\t}\n\n\t//fmt.Printf(\"Found %d entries for path %s\\n\", len(entries), dir.GetPath())\n\t//fmt.Printf(\"Found %d entries\\n\", len(entries))\n\n\tif len(entries) == 0 {\n\t\temptySlice := []model.Obj{}\n\n\t\t//fmt.Printf(\"Returning empty slice (entries): %+v\\n\", emptySlice)\n\n\t\treturn emptySlice, nil\n\t}\n\n\tvar objects []model.Obj\n\tfor _, entry := range entries {\n\t\tobj := &model.Object{\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.searchByPath(ctx, file.GetPath(), false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := d.ensureTempServer(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to start temp server: %w\", err)\n\t}\n\n\ttoken := d.generateDownloadToken(link.LinkID, file.GetName())\n\n\t/* return &model.Link{\n\t\tURL: fmt.Sprintf(\"protondrive://download/%s\", link.LinkID),\n\t}, nil */\n\n\treturn &model.Link{\n\t\tURL: fmt.Sprintf(\"http://localhost:%d/temp/%s\", d.tempServerPort, token),\n\t}, nil\n}\n\nfunc (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar parentLinkID string\n\n\tif parentDir.GetPath() == \"/\" {\n\t\tparentLinkID = d.protonDrive.RootLink.LinkID\n\t} else {\n\t\tlink, err := d.searchByPath(ctx, parentDir.GetPath(), true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparentLinkID = link.LinkID\n\t}\n\n\t_, err := d.protonDrive.CreateNewFolderByID(ctx, parentLinkID, 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\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\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.searchByPath(ctx, srcObj.GetPath(), false)\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\ttempFile, err := utils.CreateTempFile(reader, actualSize)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\tdefer tempFile.Close()\n\n\tupdatedObj := &model.Object{\n\t\tName: srcObj.GetName(),\n\t\t// Use the accurate and real size\n\t\tSize:     actualSize,\n\t\tModified: srcObj.ModTime(),\n\t\tIsFolder: false,\n\t}\n\n\treturn d.Put(ctx, dstDir, &fileStreamer{\n\t\tReadCloser: tempFile,\n\t\tobj:        updatedObj,\n\t}, nil)\n}\n\nfunc (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error {\n\tlink, err := d.searchByPath(ctx, obj.GetPath(), obj.IsDir())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif obj.IsDir() {\n\t\treturn d.protonDrive.MoveFolderToTrashByID(ctx, link.LinkID, false)\n\t} else {\n\t\treturn d.protonDrive.MoveFileToTrashByID(ctx, link.LinkID)\n\t}\n}\n\nfunc (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tvar parentLinkID string\n\n\tif dstDir.GetPath() == \"/\" {\n\t\tparentLinkID = d.protonDrive.RootLink.LinkID\n\t} else {\n\t\tlink, err := d.searchByPath(ctx, dstDir.GetPath(), true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparentLinkID = link.LinkID\n\t}\n\n\ttempFile, err := utils.CreateTempFile(file, file.GetSize())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\tdefer tempFile.Close()\n\n\terr = d.uploadFile(ctx, parentLinkID, file.GetName(), tempFile, file.GetSize(), up)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuploadedObj := &model.Object{\n\t\tName:     file.GetName(),\n\t\tSize:     file.GetSize(),\n\t\tModified: file.ModTime(),\n\t\tIsFolder: false,\n\t}\n\treturn uploadedObj, nil\n}\n\nfunc (d *ProtonDrive) 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 *ProtonDrive) 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 *ProtonDrive) 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 *ProtonDrive) 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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootPath\n\t//driver.RootID\n\n\tUsername  string `json:\"username\" required:\"true\" type:\"string\"`\n\tPassword  string `json:\"password\" required:\"true\" type:\"string\"`\n\tTwoFACode string `json:\"two_fa_code,omitempty\" type:\"string\"`\n}\n\ntype Config struct {\n\tName        string `json:\"name\"`\n\tLocalSort   bool   `json:\"local_sort\"`\n\tOnlyLocal   bool   `json:\"only_local\"`\n\tOnlyProxy   bool   `json:\"only_proxy\"`\n\tNoCache     bool   `json:\"no_cache\"`\n\tNoUpload    bool   `json:\"no_upload\"`\n\tNeedMs      bool   `json:\"need_ms\"`\n\tDefaultRoot string `json:\"default_root\"`\n}\n\nvar config = driver.Config{\n\tName:              \"ProtonDrive\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"/\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &ProtonDrive{\n\t\t\tapiBase:             \"https://drive.proton.me/api\",\n\t\t\tappVersion:          \"windows-drive@1.11.3+rclone+proton\",\n\t\t\tcredentialCacheFile: \".prtcrd\",\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\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/henrybear327/go-proton-api\"\n)\n\ntype ProtonFile struct {\n\t*proton.Link\n\tName     string\n\tIsFolder bool\n}\n\nfunc (p *ProtonFile) GetName() string {\n\treturn p.Name\n}\n\nfunc (p *ProtonFile) GetSize() int64 {\n\treturn p.Link.Size\n}\n\nfunc (p *ProtonFile) GetPath() string {\n\treturn p.Name\n}\n\nfunc (p *ProtonFile) IsDir() bool {\n\treturn p.IsFolder\n}\n\nfunc (p *ProtonFile) ModTime() time.Time {\n\treturn time.Unix(p.Link.ModifyTime, 0)\n}\n\nfunc (p *ProtonFile) CreateTime() time.Time {\n\treturn time.Unix(p.Link.CreateTime, 0)\n}\n\ntype downloadInfo struct {\n\tLinkID   string\n\tFileName string\n}\n\ntype fileStreamer struct {\n\tio.ReadCloser\n\tobj model.Obj\n}\n\nfunc (fs *fileStreamer) GetMimetype() string       { return \"\" }\nfunc (fs *fileStreamer) NeedStore() bool           { return false }\nfunc (fs *fileStreamer) IsForceStreamUpload() bool { return false }\nfunc (fs *fileStreamer) GetExist() model.Obj       { return nil }\nfunc (fs *fileStreamer) SetExist(model.Obj)        {}\nfunc (fs *fileStreamer) RangeRead(http_range.Range) (io.Reader, error) {\n\treturn nil, errors.New(\"not supported\")\n}\nfunc (fs *fileStreamer) CacheFullInTempFile() (model.File, error) {\n\treturn nil, errors.New(\"not supported\")\n}\nfunc (fs *fileStreamer) SetTmpFile(r *os.File)   {}\nfunc (fs *fileStreamer) GetFile() model.File     { return nil }\nfunc (fs *fileStreamer) GetName() string         { return fs.obj.GetName() }\nfunc (fs *fileStreamer) GetSize() int64          { return fs.obj.GetSize() }\nfunc (fs *fileStreamer) GetPath() string         { return fs.obj.GetPath() }\nfunc (fs *fileStreamer) IsDir() bool             { return fs.obj.IsDir() }\nfunc (fs *fileStreamer) ModTime() time.Time      { return fs.obj.ModTime() }\nfunc (fs *fileStreamer) CreateTime() time.Time   { return fs.obj.ModTime() }\nfunc (fs *fileStreamer) GetHash() utils.HashInfo { return fs.obj.GetHash() }\nfunc (fs *fileStreamer) GetID() string           { return fs.obj.GetID() }\n\ntype httpRange struct {\n\tstart, end int64\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 progressReader struct {\n\treader   io.Reader\n\ttotal    int64\n\tcurrent  int64\n\tcallback driver.UpdateProgress\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/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ProtonMail/gopenpgp/v2/crypto\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/henrybear327/Proton-API-Bridge/common\"\n\t\"github.com/henrybear327/go-proton-api\"\n)\n\nfunc (d *ProtonDrive) loadCachedCredentials() (*common.ReusableCredentialData, error) {\n\tif d.credentialCacheFile == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tif _, err := os.Stat(d.credentialCacheFile); os.IsNotExist(err) {\n\t\treturn nil, nil\n\t}\n\n\tdata, err := os.ReadFile(d.credentialCacheFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read credential cache file: %w\", err)\n\t}\n\n\tvar credentials common.ReusableCredentialData\n\tif err := json.Unmarshal(data, &credentials); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse cached credentials: %w\", err)\n\t}\n\n\tif credentials.UID == \"\" || credentials.AccessToken == \"\" ||\n\t\tcredentials.RefreshToken == \"\" || credentials.SaltedKeyPass == \"\" {\n\t\treturn nil, fmt.Errorf(\"cached credentials are incomplete\")\n\t}\n\n\treturn &credentials, nil\n}\n\nfunc (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolder bool) (*proton.Link, error) {\n\tif fullPath == \"/\" {\n\t\treturn d.protonDrive.RootLink, nil\n\t}\n\n\tcleanPath := strings.Trim(fullPath, \"/\")\n\tpathParts := strings.Split(cleanPath, \"/\")\n\n\tcurrentLink := d.protonDrive.RootLink\n\n\tfor i, part := range pathParts {\n\t\tisLastPart := i == len(pathParts)-1\n\t\tsearchForFolder := !isLastPart || isFolder\n\n\t\tentries, err := d.protonDrive.ListDirectory(ctx, currentLink.LinkID)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list directory: %w\", err)\n\n\t\t}\n\n\t\tfound := false\n\t\tfor _, entry := range entries {\n\t\t\t// entry.Name is already decrypted!\n\t\t\tif entry.Name == part && entry.IsFolder == searchForFolder {\n\t\t\t\tcurrentLink = entry.Link\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\treturn nil, fmt.Errorf(\"path not found: %s (looking for part: %s)\", fullPath, part)\n\t\t}\n\t}\n\n\treturn currentLink, nil\n}\n\nfunc (pr *progressReader) Read(p []byte) (int, error) {\n\tn, err := pr.reader.Read(p)\n\tpr.current += int64(n)\n\n\tif pr.callback != nil {\n\t\tpercentage := float64(pr.current) / float64(pr.total) * 100\n\t\tpr.callback(percentage)\n\t}\n\n\treturn n, err\n}\n\nfunc (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID, fileName string, file *os.File, size int64, up driver.UpdateProgress) error {\n\n\tfileInfo, err := file.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get file info: %w\", err)\n\t}\n\n\t_, err = d.protonDrive.GetLink(ctx, parentLinkID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get parent link: %w\", err)\n\t}\n\n\treader := &progressReader{\n\t\treader:   bufio.NewReader(file),\n\t\ttotal:    size,\n\t\tcurrent:  0,\n\t\tcallback: up,\n\t}\n\n\t_, _, err = d.protonDrive.UploadFileByReader(ctx, parentLinkID, fileName, fileInfo.ModTime(), reader, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) ensureTempServer() error {\n\tif d.tempServer != nil {\n\n\t\t// Already running\n\t\treturn nil\n\t}\n\n\tlistener, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\treturn err\n\t}\n\td.tempServerPort = listener.Addr().(*net.TCPAddr).Port\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/temp/\", d.handleTempDownload)\n\n\td.tempServer = &http.Server{\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\td.tempServer.Serve(listener)\n\t}()\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) handleTempDownload(w http.ResponseWriter, r *http.Request) {\n\ttoken := strings.TrimPrefix(r.URL.Path, \"/temp/\")\n\n\td.tokenMutex.RLock()\n\tinfo, exists := d.downloadTokens[token]\n\td.tokenMutex.RUnlock()\n\n\tif !exists {\n\t\thttp.Error(w, \"Invalid or expired token\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlink, err := d.protonDrive.GetLink(r.Context(), info.LinkID)\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to get file link\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Get file size for range calculations\n\t_, _, attrs, err := d.protonDrive.DownloadFile(r.Context(), link, 0)\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to get file info\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfileSize := attrs.Size\n\n\trangeHeader := r.Header.Get(\"Range\")\n\tif rangeHeader != \"\" {\n\n\t\t// Parse range header like \"bytes=0-1023\" or \"bytes=1024-\"\n\t\tranges, err := parseRange(rangeHeader, fileSize)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Invalid range\", http.StatusRequestedRangeNotSatisfiable)\n\t\t\treturn\n\t\t}\n\n\t\tif len(ranges) == 1 {\n\n\t\t\t// Single range request, small\n\t\t\tstart, end := ranges[0].start, ranges[0].end\n\t\t\tcontentLength := end - start + 1\n\n\t\t\t// Start download from offset\n\t\t\treader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, start)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Failed to start download\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer reader.Close()\n\n\t\t\tw.Header().Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", start, end, fileSize))\n\t\t\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", contentLength))\n\t\t\tw.Header().Set(\"Content-Type\", mime.TypeByExtension(filepath.Ext(link.Name)))\n\n\t\t\t// Partial content...\n\t\t\t// Setting fileName is more cosmetical here\n\t\t\t//.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", link.Name))\n\t\t\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", info.FileName))\n\t\t\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\n\t\t\tw.WriteHeader(http.StatusPartialContent)\n\n\t\t\tio.CopyN(w, reader, contentLength)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Full file download (non-range request)\n\treader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, 0)\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to start download\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer reader.Close()\n\n\t// Set headers for full content\n\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", fileSize))\n\tw.Header().Set(\"Content-Type\", mime.TypeByExtension(filepath.Ext(link.Name)))\n\n\t// Setting fileName is needed since ProtonDrive fileName is more like a random string\n\t//w.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", link.Name))\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", info.FileName))\n\n\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\n\t// Stream the full file\n\tio.Copy(w, reader)\n}\n\nfunc (d *ProtonDrive) generateDownloadToken(linkID, fileName string) string {\n\ttoken := fmt.Sprintf(\"%d_%s\", time.Now().UnixNano(), linkID[:8])\n\n\td.tokenMutex.Lock()\n\tif d.downloadTokens == nil {\n\t\td.downloadTokens = make(map[string]*downloadInfo)\n\t}\n\n\td.downloadTokens[token] = &downloadInfo{\n\t\tLinkID:   linkID,\n\t\tFileName: fileName,\n\t}\n\n\td.tokenMutex.Unlock()\n\n\tgo func() {\n\n\t\t// Token expires in 1 hour\n\t\ttime.Sleep(1 * time.Hour)\n\t\td.tokenMutex.Lock()\n\n\t\tdelete(d.downloadTokens, token)\n\t\td.tokenMutex.Unlock()\n\t}()\n\n\treturn token\n}\n\nfunc parseRange(rangeHeader string, size int64) ([]httpRange, error) {\n\tif !strings.HasPrefix(rangeHeader, \"bytes=\") {\n\t\treturn nil, fmt.Errorf(\"invalid range header\")\n\t}\n\n\trangeSpec := strings.TrimPrefix(rangeHeader, \"bytes=\")\n\tranges := strings.Split(rangeSpec, \",\")\n\n\tvar result []httpRange\n\tfor _, r := range ranges {\n\t\tr = strings.TrimSpace(r)\n\t\tif strings.Contains(r, \"-\") {\n\t\t\tparts := strings.Split(r, \"-\")\n\t\t\tif len(parts) != 2 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid range format\")\n\t\t\t}\n\n\t\t\tvar start, end int64\n\t\t\tvar err error\n\n\t\t\tif parts[0] == \"\" {\n\n\t\t\t\t// Suffix range (e.g., \"-500\")\n\t\t\t\tif parts[1] == \"\" {\n\t\t\t\t\treturn nil, fmt.Errorf(\"invalid range format\")\n\t\t\t\t}\n\t\t\t\tend = size - 1\n\t\t\t\tstart, err = strconv.ParseInt(parts[1], 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tstart = size - start\n\t\t\t\tif start < 0 {\n\t\t\t\t\tstart = 0\n\t\t\t\t}\n\t\t\t} else if parts[1] == \"\" {\n\n\t\t\t\t// Prefix range (e.g., \"500-\")\n\t\t\t\tstart, err = strconv.ParseInt(parts[0], 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tend = size - 1\n\t\t\t} else {\n\t\t\t\t// Full range (e.g., \"0-1023\")\n\t\t\t\tstart, err = strconv.ParseInt(parts[0], 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tend, err = strconv.ParseInt(parts[1], 10, 64)\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\n\t\t\tif start >= size || end >= size || start > end {\n\t\t\t\treturn nil, fmt.Errorf(\"range out of bounds\")\n\t\t\t}\n\n\t\t\tresult = append(result, httpRange{start: start, end: end})\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) {\n\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\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\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\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.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir())\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\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\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.credentials.UID)\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+d.credentials.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.credentials.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.credentials.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.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find source: %w\", err)\n\t}\n\n\tvar dstParentLinkID string\n\tif dstDir.GetPath() == \"/\" {\n\t\tdstParentLinkID = d.RootLink.LinkID\n\t} else {\n\t\tdstLink, err := d.searchByPath(ctx, dstDir.GetPath(), true)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to find destination: %w\", err)\n\t\t}\n\t\tdstParentLinkID = dstLink.LinkID\n\t}\n\n\tif srcObj.IsDir() {\n\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\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\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.RootLink.LinkID {\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"
  },
  {
    "path": "drivers/quark_uc/driver.go",
    "content": "package quark\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\tstreamPkg \"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\tlog \"github.com/sirupsen/logrus\"\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 && d.AdditionVersion != 2 {\n\t\td.AdditionVersion = 2\n\t\tif !d.UseTransCodingAddress && len(d.DownProxyUrl) == 0 {\n\t\t\td.WebProxy = true\n\t\t\td.WebdavPolicy = \"native_proxy\"\n\t\t}\n\t\top.MustSaveDriverStorage(d)\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\treturn d.GetFiles(dir.GetID())\n}\n\nfunc (d *QuarkOrUC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tf := file.(*File)\n\tif d.UseTransCodingAddress && d.config.Name == \"Quark\" && f.Category == 1 && f.Size > 0 {\n\t\tlink, err := d.getTranscodingLink(file)\n\t\tif err == nil {\n\t\t\treturn link, nil\n\t\t}\n\t\tif strings.Contains(err.Error(), \"plf_invalid\") {\n\t\t\tlog.Warnf(\"quark transcoding link invalid for %s, fallback to download link: %v\", file.GetName(), err)\n\t\t\treturn d.getDownloadLink(file)\n\t\t}\n\t\treturn nil, err\n\t}\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 := streamPkg.CacheFullInTempFileAndWriter(stream, 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\tlog.Debugln(\"hash: \", md5Str, sha1Str)\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\treturn nil\n\t}\n\t// part up\n\ttotal := stream.GetSize()\n\tleft := total\n\tpartSize := int64(pre.Metadata.PartSize)\n\tpart := make([]byte, partSize)\n\tcount := int(total / partSize)\n\tif total%partSize > 0 {\n\t\tcount++\n\t}\n\tmd5s := make([]string, 0, count)\n\tpartNumber := 1\n\tfor left > 0 {\n\t\tif utils.IsCanceled(ctx) {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tif left < partSize {\n\t\t\tpart = part[:left]\n\t\t}\n\t\tn, err := io.ReadFull(stream, part)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tleft -= int64(n)\n\t\tlog.Debugf(\"left: %d\", left)\n\t\treader := driver.NewLimitedUploadStream(ctx, bytes.NewReader(part))\n\t\tm, err := d.upPart(ctx, pre, stream.GetMimetype(), partNumber, reader)\n\t\t//m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif m == \"finish\" {\n\t\t\treturn nil\n\t\t}\n\t\tmd5s = append(md5s, m)\n\t\tpartNumber++\n\t\tup(100 * float64(total-left) / float64(total))\n\t}\n\terr = d.upCommit(pre, md5s)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn d.upFinish(pre)\n}\n\nvar _ driver.Driver = (*QuarkOrUC)(nil)\n"
  },
  {
    "path": "drivers/quark_uc/meta.go",
    "content": "package quark\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\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\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\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} `json:\"video_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} `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 {\n\t} `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 {\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/cookie\"\n\t\"github.com/alist-org/alist/v3/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\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\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\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\t\tif page*size >= resp.Metadata.Total {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\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\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\tres, err := base.RestyClient.R().SetContext(ctx).\n\t\tSetHeaders(map[string]string{\n\t\t\t\"Authorization\":    resp.Data.AuthKey,\n\t\t\t\"Content-Type\":     mineType,\n\t\t\t\"Referer\":          \"https://pan.quark.cn/\",\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\"partNumber\": strconv.Itoa(partNumber),\n\t\t\t\"uploadId\":   pre.Data.UploadId,\n\t\t}).SetBody(bytes).Put(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 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"
  },
  {
    "path": "drivers/quark_uc_tv/driver.go",
    "content": "package quark_uc_tv\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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\tfor {\n\t\tvar filesData FilesData\n\t\t_, err := d.request(ctx, \"/file\", \"GET\", 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\":   \"3\",\n\t\t\t\t\"desc\":       \"1\",\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\tvar fileLink FileLink\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\t\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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:\"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}\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\tOnlyLocal:         false,\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.5.6\",\n\t\t\t\tchannel:  \"CP\",\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\tOnlyLocal:         false,\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.6.5\",\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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 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 FileLink 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\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\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\tif e.Status == -1 && e.Errno == 10001 {\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, \"GET\", 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, \"GET\", 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(\"POST\", 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"
  },
  {
    "path": "drivers/quqi/driver.go",
    "content": "package quqi\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\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\"\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 Quqi struct {\n\tmodel.Storage\n\tAddition\n\tCookie   string // Cookie\n\tGroupID  string // 私人云群组ID\n\tClientID string // 随机生成客户端ID 经过测试，部分接口调用若不携带client id会出现错误\n}\n\nfunc (d *Quqi) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Quqi) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Quqi) Init(ctx context.Context) error {\n\t// 登录\n\tif err := d.login(); err != nil {\n\t\treturn err\n\t}\n\n\t// 生成随机client id (与网页端生成逻辑一致)\n\td.ClientID = \"quqipc_\" + random.String(10)\n\n\t// 获取私人云ID (暂时仅获取私人云)\n\tgroupResp := &GroupRes{}\n\tif _, err := d.request(\"group.quqi.com\", \"/v1/group/list\", resty.MethodGet, nil, groupResp); err != nil {\n\t\treturn err\n\t}\n\tfor _, groupInfo := range groupResp.Data {\n\t\tif groupInfo == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif groupInfo.Type == 2 {\n\t\t\td.GroupID = strconv.Itoa(groupInfo.ID)\n\t\t\tbreak\n\t\t}\n\t}\n\tif d.GroupID == \"\" {\n\t\treturn errs.StorageNotFound\n\t}\n\n\treturn nil\n}\n\nfunc (d *Quqi) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Quqi) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tvar (\n\t\tlistResp = &ListRes{}\n\t\tfiles    []model.Obj\n\t)\n\n\tif _, err := d.request(\"\", \"/api/dir/ls\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"node_id\":   dir.GetID(),\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, listResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif listResp.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\t// dirs\n\tfor _, dirInfo := range listResp.Data.Dir {\n\t\tif dirInfo == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfiles = append(files, &model.Object{\n\t\t\tID:       strconv.FormatInt(dirInfo.NodeID, 10),\n\t\t\tName:     dirInfo.Name,\n\t\t\tModified: time.Unix(dirInfo.UpdateTime, 0),\n\t\t\tCtime:    time.Unix(dirInfo.AddTime, 0),\n\t\t\tIsFolder: true,\n\t\t})\n\t}\n\n\t// files\n\tfor _, fileInfo := range listResp.Data.File {\n\t\tif fileInfo == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif fileInfo.EXT != \"\" {\n\t\t\tfileInfo.Name = strings.Join([]string{fileInfo.Name, fileInfo.EXT}, \".\")\n\t\t}\n\n\t\tfiles = append(files, &model.Object{\n\t\t\tID:       strconv.FormatInt(fileInfo.NodeID, 10),\n\t\t\tName:     fileInfo.Name,\n\t\t\tSize:     fileInfo.Size,\n\t\t\tModified: time.Unix(fileInfo.UpdateTime, 0),\n\t\t\tCtime:    time.Unix(fileInfo.AddTime, 0),\n\t\t})\n\t}\n\n\treturn files, nil\n}\n\nfunc (d *Quqi) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif d.CDN {\n\t\tlink, err := d.linkFromCDN(file.GetID())\n\t\tif err != nil {\n\t\t\tlog.Warn(err)\n\t\t} else {\n\t\t\treturn link, nil\n\t\t}\n\t}\n\n\tlink, err := d.linkFromPreview(file.GetID())\n\tif err != nil {\n\t\tlog.Warn(err)\n\t} else {\n\t\treturn link, nil\n\t}\n\n\tlink, err = d.linkFromDownload(file.GetID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn link, nil\n}\n\nfunc (d *Quqi) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tvar (\n\t\tmakeDirRes = &MakeDirRes{}\n\t\ttimeNow    = time.Now()\n\t)\n\n\tif _, err := d.request(\"\", \"/api/dir/mkDir\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"parent_id\": parentDir.GetID(),\n\t\t\t\"name\":      dirName,\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, makeDirRes); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       strconv.FormatInt(makeDirRes.Data.NodeID, 10),\n\t\tName:     dirName,\n\t\tModified: timeNow,\n\t\tCtime:    timeNow,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (d *Quqi) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tvar moveRes = &MoveRes{}\n\n\tif _, err := d.request(\"\", \"/api/dir/mvDir\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":        d.GroupID,\n\t\t\t\"tree_id\":        \"1\",\n\t\t\t\"node_id\":        dstDir.GetID(),\n\t\t\t\"source_quqi_id\": d.GroupID,\n\t\t\t\"source_tree_id\": \"1\",\n\t\t\t\"source_node_id\": srcObj.GetID(),\n\t\t\t\"client_id\":      d.ClientID,\n\t\t})\n\t}, moveRes); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       strconv.FormatInt(moveRes.Data.NodeID, 10),\n\t\tName:     moveRes.Data.NodeName,\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 *Quqi) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tvar realName = newName\n\n\tif !srcObj.IsDir() {\n\t\tsrcExt, newExt := utils.Ext(srcObj.GetName()), utils.Ext(newName)\n\n\t\t// 曲奇网盘的文件名称由文件名和扩展名组成，若存在扩展名，则重命名时仅支持更改文件名，扩展名在曲奇服务端保留\n\t\tif srcExt != \"\" && srcExt == newExt {\n\t\t\tparts := strings.Split(newName, \".\")\n\t\t\tif len(parts) > 1 {\n\t\t\t\trealName = strings.Join(parts[:len(parts)-1], \".\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, err := d.request(\"\", \"/api/dir/renameDir\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"node_id\":   srcObj.GetID(),\n\t\t\t\"rename\":    realName,\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, nil); 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: time.Now(),\n\t\tCtime:    srcObj.CreateTime(),\n\t\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *Quqi) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\t// 无法从曲奇接口响应中直接获取复制后的文件信息\n\tif _, err := d.request(\"\", \"/api/node/copy\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":        d.GroupID,\n\t\t\t\"tree_id\":        \"1\",\n\t\t\t\"node_id\":        dstDir.GetID(),\n\t\t\t\"source_quqi_id\": d.GroupID,\n\t\t\t\"source_tree_id\": \"1\",\n\t\t\t\"source_node_id\": srcObj.GetID(),\n\t\t\t\"client_id\":      d.ClientID,\n\t\t})\n\t}, nil); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *Quqi) Remove(ctx context.Context, obj model.Obj) error {\n\t// 暂时不做直接删除，默认都放到回收站。直接删除方法：先调用删除接口放入回收站，在通过回收站接口删除文件\n\tif _, err := d.request(\"\", \"/api/node/del\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"node_id\":   obj.GetID(),\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, nil); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Quqi) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\t// base info\n\tsizeStr := strconv.FormatInt(stream.GetSize(), 10)\n\tf, err := stream.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmd5, err := utils.HashFile(utils.MD5, f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsha, err := utils.HashFile(utils.SHA256, f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// init upload\n\tvar uploadInitResp UploadInitResp\n\t_, err = d.request(\"\", \"/api/upload/v1/file/init\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"parent_id\": dstDir.GetID(),\n\t\t\t\"size\":      sizeStr,\n\t\t\t\"file_name\": stream.GetName(),\n\t\t\t\"md5\":       md5,\n\t\t\t\"sha\":       sha,\n\t\t\t\"is_slice\":  \"true\",\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, &uploadInitResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// check exist\n\t// if the file already exists in Quqi server, there is no need to actually upload it\n\tif uploadInitResp.Data.Exist {\n\t\t// the file name returned by Quqi does not include the extension name\n\t\tnodeName, nodeExt := uploadInitResp.Data.NodeName, utils.Ext(stream.GetName())\n\t\tif nodeExt != \"\" {\n\t\t\tnodeName = nodeName + \".\" + nodeExt\n\t\t}\n\t\treturn &model.Object{\n\t\t\tID:       strconv.FormatInt(uploadInitResp.Data.NodeID, 10),\n\t\t\tName:     nodeName,\n\t\t\tSize:     stream.GetSize(),\n\t\t\tModified: stream.ModTime(),\n\t\t\tCtime:    stream.CreateTime(),\n\t\t}, nil\n\t}\n\t// listParts\n\t_, err = d.request(\"upload.quqi.com:20807\", \"/upload/v1/listParts\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"token\":     uploadInitResp.Data.Token,\n\t\t\t\"task_id\":   uploadInitResp.Data.TaskID,\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// get temp key\n\tvar tempKeyResp TempKeyResp\n\t_, err = d.request(\"upload.quqi.com:20807\", \"/upload/v1/tempKey\", resty.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"token\":   uploadInitResp.Data.Token,\n\t\t\t\"task_id\": uploadInitResp.Data.TaskID,\n\t\t})\n\t}, &tempKeyResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// upload\n\t// u, err := url.Parse(fmt.Sprintf(\"https://%s.cos.ap-shanghai.myqcloud.com\", uploadInitResp.Data.Bucket))\n\t// b := &cos.BaseURL{BucketURL: u}\n\t// client := cos.NewClient(b, &http.Client{\n\t// \tTransport: &cos.CredentialTransport{\n\t// \t\tCredential: cos.NewTokenCredential(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken),\n\t// \t},\n\t// })\n\t// partSize := int64(1024 * 1024 * 2)\n\t// partCount := (stream.GetSize() + partSize - 1) / partSize\n\t// for i := 1; i <= int(partCount); i++ {\n\t// \tlength := partSize\n\t// \tif i == int(partCount) {\n\t// \t\tlength = stream.GetSize() - (int64(i)-1)*partSize\n\t// \t}\n\t// \t_, err := client.Object.UploadPart(\n\t// \t\tctx, uploadInitResp.Data.Key, uploadInitResp.Data.UploadID, i, io.LimitReader(f, partSize), &cos.ObjectUploadPartOptions{\n\t// \t\t\tContentLength: length,\n\t// \t\t},\n\t// \t)\n\t// \tif err != nil {\n\t// \t\treturn nil, err\n\t// \t}\n\t// }\n\n\tcfg := &aws.Config{\n\t\tCredentials: credentials.NewStaticCredentials(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken),\n\t\tRegion:      aws.String(\"ap-shanghai\"),\n\t\tEndpoint:    aws.String(\"cos.ap-shanghai.myqcloud.com\"),\n\t}\n\ts, err := session.NewSession(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuploader := s3manager.NewUploader(s)\n\tbuf := make([]byte, 1024*1024*2)\n\tfup := &driver.ReaderUpdatingProgress{\n\t\tReader: &driver.SimpleReaderWithSize{\n\t\t\tReader: f,\n\t\t\tSize:   int64(len(buf)),\n\t\t},\n\t\tUpdateProgress: up,\n\t}\n\tfor partNumber := int64(1); ; partNumber++ {\n\t\tn, err := io.ReadFull(fup, buf)\n\t\tif err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\treader := bytes.NewReader(buf[:n])\n\t\t_, err = uploader.S3.UploadPartWithContext(ctx, &s3.UploadPartInput{\n\t\t\tUploadId:   &uploadInitResp.Data.UploadID,\n\t\t\tKey:        &uploadInitResp.Data.Key,\n\t\t\tBucket:     &uploadInitResp.Data.Bucket,\n\t\t\tPartNumber: aws.Int64(partNumber),\n\t\t\tBody: struct {\n\t\t\t\t*driver.RateLimitReader\n\t\t\t\tio.Seeker\n\t\t\t}{\n\t\t\t\tRateLimitReader: driver.NewLimitedUploadStream(ctx, reader),\n\t\t\t\tSeeker:          reader,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// finish upload\n\tvar uploadFinishResp UploadFinishResp\n\t_, err = d.request(\"\", \"/api/upload/v1/file/finish\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"token\":     uploadInitResp.Data.Token,\n\t\t\t\"task_id\":   uploadInitResp.Data.TaskID,\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, &uploadFinishResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// the file name returned by Quqi does not include the extension name\n\tnodeName, nodeExt := uploadFinishResp.Data.NodeName, utils.Ext(stream.GetName())\n\tif nodeExt != \"\" {\n\t\tnodeName = nodeName + \".\" + nodeExt\n\t}\n\treturn &model.Object{\n\t\tID:       strconv.FormatInt(uploadFinishResp.Data.NodeID, 10),\n\t\tName:     nodeName,\n\t\tSize:     stream.GetSize(),\n\t\tModified: stream.ModTime(),\n\t\tCtime:    stream.CreateTime(),\n\t}, nil\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 = (*Quqi)(nil)\n"
  },
  {
    "path": "drivers/quqi/meta.go",
    "content": "package quqi\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tPhone    string `json:\"phone\"`\n\tPassword string `json:\"password\"`\n\tCookie   string `json:\"cookie\" help:\"Cookie can be used on multiple clients at the same time\"`\n\tCDN      bool   `json:\"cdn\" help:\"If you enable this option, the download speed can be increased, but there will be some performance loss\"`\n}\n\nvar config = driver.Config{\n\tName:      \"Quqi\",\n\tOnlyLocal: true,\n\tLocalSort: true,\n\t//NoUpload:    true,\n\tDefaultRoot: \"0\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Quqi{}\n\t})\n}\n"
  },
  {
    "path": "drivers/quqi/types.go",
    "content": "package quqi\n\ntype BaseReqQuery struct {\n\tID string `json:\"quqiid\"`\n}\n\ntype BaseReq struct {\n\tGroupID string `json:\"quqi_id\"`\n}\n\ntype BaseRes struct {\n\t//Data    interface{} `json:\"data\"`\n\tCode    int    `json:\"err\"`\n\tMessage string `json:\"msg\"`\n}\n\ntype GroupRes struct {\n\tBaseRes\n\tData []*Group `json:\"data\"`\n}\n\ntype ListRes struct {\n\tBaseRes\n\tData *List `json:\"data\"`\n}\n\ntype GetDocRes struct {\n\tBaseRes\n\tData struct {\n\t\tOriginPath string `json:\"origin_path\"`\n\t} `json:\"data\"`\n}\n\ntype GetDownloadResp struct {\n\tBaseRes\n\tData struct {\n\t\tUrl string `json:\"url\"`\n\t} `json:\"data\"`\n}\n\ntype MakeDirRes struct {\n\tBaseRes\n\tData struct {\n\t\tIsRoot   bool  `json:\"is_root\"`\n\t\tNodeID   int64 `json:\"node_id\"`\n\t\tParentID int64 `json:\"parent_id\"`\n\t} `json:\"data\"`\n}\n\ntype MoveRes struct {\n\tBaseRes\n\tData struct {\n\t\tNodeChildNum int64  `json:\"node_child_num\"`\n\t\tNodeID       int64  `json:\"node_id\"`\n\t\tNodeName     string `json:\"node_name\"`\n\t\tParentID     int64  `json:\"parent_id\"`\n\t\tGroupID      int64  `json:\"quqi_id\"`\n\t\tTreeID       int64  `json:\"tree_id\"`\n\t} `json:\"data\"`\n}\n\ntype RenameRes struct {\n\tBaseRes\n\tData struct {\n\t\tNodeID     int64  `json:\"node_id\"`\n\t\tGroupID    int64  `json:\"quqi_id\"`\n\t\tRename     string `json:\"rename\"`\n\t\tTreeID     int64  `json:\"tree_id\"`\n\t\tUpdateTime int64  `json:\"updatetime\"`\n\t} `json:\"data\"`\n}\n\ntype CopyRes struct {\n\tBaseRes\n}\n\ntype RemoveRes struct {\n\tBaseRes\n}\n\ntype Group struct {\n\tID              int    `json:\"quqi_id\"`\n\tType            int    `json:\"type\"`\n\tName            string `json:\"name\"`\n\tIsAdministrator int    `json:\"is_administrator\"`\n\tRole            []int  `json:\"role\"`\n\tAvatar          string `json:\"avatar_url\"`\n\tIsStick         int    `json:\"is_stick\"`\n\tNickname        string `json:\"nickname\"`\n\tStatus          int    `json:\"status\"`\n}\n\ntype List struct {\n\tListDir\n\tDir  []*ListDir  `json:\"dir\"`\n\tFile []*ListFile `json:\"file\"`\n}\n\ntype ListItem struct {\n\tAddTime        int64  `json:\"add_time\"`\n\tIsDir          int    `json:\"is_dir\"`\n\tIsExpand       int    `json:\"is_expand\"`\n\tIsFinalize     int    `json:\"is_finalize\"`\n\tLastEditorName string `json:\"last_editor_name\"`\n\tName           string `json:\"name\"`\n\tNodeID         int64  `json:\"nid\"`\n\tParentID       int64  `json:\"parent_id\"`\n\tPermission     int    `json:\"permission\"`\n\tTreeID         int64  `json:\"tid\"`\n\tUpdateCNT      int64  `json:\"update_cnt\"`\n\tUpdateTime     int64  `json:\"update_time\"`\n}\n\ntype ListDir struct {\n\tListItem\n\tChildDocNum int64  `json:\"child_doc_num\"`\n\tDirDetail   string `json:\"dir_detail\"`\n\tDirType     int    `json:\"dir_type\"`\n}\n\ntype ListFile struct {\n\tListItem\n\tBroadDocType       string `json:\"broad_doc_type\"`\n\tCanDisplay         bool   `json:\"can_display\"`\n\tDetail             string `json:\"detail\"`\n\tEXT                string `json:\"ext\"`\n\tFiletype           string `json:\"filetype\"`\n\tHasMobileThumbnail bool   `json:\"has_mobile_thumbnail\"`\n\tHasThumbnail       bool   `json:\"has_thumbnail\"`\n\tSize               int64  `json:\"size\"`\n\tVersion            int    `json:\"version\"`\n}\n\ntype UploadInitResp struct {\n\tData struct {\n\t\tBucket   string `json:\"bucket\"`\n\t\tExist    bool   `json:\"exist\"`\n\t\tKey      string `json:\"key\"`\n\t\tTaskID   string `json:\"task_id\"`\n\t\tToken    string `json:\"token\"`\n\t\tUploadID string `json:\"upload_id\"`\n\t\tURL      string `json:\"url\"`\n\t\tNodeID   int64  `json:\"node_id\"`\n\t\tNodeName string `json:\"node_name\"`\n\t\tParentID int64  `json:\"parent_id\"`\n\t} `json:\"data\"`\n\tErr int    `json:\"err\"`\n\tMsg string `json:\"msg\"`\n}\n\ntype TempKeyResp struct {\n\tErr  int    `json:\"err\"`\n\tMsg  string `json:\"msg\"`\n\tData struct {\n\t\tExpiredTime int    `json:\"expiredTime\"`\n\t\tExpiration  string `json:\"expiration\"`\n\t\tCredentials struct {\n\t\t\tSessionToken string `json:\"sessionToken\"`\n\t\t\tTmpSecretID  string `json:\"tmpSecretId\"`\n\t\t\tTmpSecretKey string `json:\"tmpSecretKey\"`\n\t\t} `json:\"credentials\"`\n\t\tRequestID string `json:\"requestId\"`\n\t\tStartTime int    `json:\"startTime\"`\n\t} `json:\"data\"`\n}\n\ntype UploadFinishResp struct {\n\tData struct {\n\t\tNodeID   int64  `json:\"node_id\"`\n\t\tNodeName string `json:\"node_name\"`\n\t\tParentID int64  `json:\"parent_id\"`\n\t\tQuqiID   int64  `json:\"quqi_id\"`\n\t\tTreeID   int64  `json:\"tree_id\"`\n\t} `json:\"data\"`\n\tErr int    `json:\"err\"`\n\tMsg string `json:\"msg\"`\n}\n\ntype UrlExchangeResp struct {\n\tBaseRes\n\tData struct {\n\t\tName               string `json:\"name\"`\n\t\tMime               string `json:\"mime\"`\n\t\tSize               int64  `json:\"size\"`\n\t\tDownloadType       int    `json:\"download_type\"`\n\t\tChannelType        int    `json:\"channel_type\"`\n\t\tChannelID          int    `json:\"channel_id\"`\n\t\tUrl                string `json:\"url\"`\n\t\tExpiredTime        int64  `json:\"expired_time\"`\n\t\tIsEncrypted        bool   `json:\"is_encrypted\"`\n\t\tEncryptedSize      int64  `json:\"encrypted_size\"`\n\t\tEncryptedAlg       string `json:\"encrypted_alg\"`\n\t\tEncryptedKey       string `json:\"encrypted_key\"`\n\t\tPassportID         int64  `json:\"passport_id\"`\n\t\tRequestExpiredTime int64  `json:\"request_expired_time\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "drivers/quqi/util.go",
    "content": "package quqi\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/minio/sio\"\n)\n\n// do others that not defined in Driver interface\nfunc (d *Quqi) request(host string, path string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {\n\tvar (\n\t\treqUrl = url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"quqi.com\",\n\t\t\tPath:   path,\n\t\t}\n\t\treq    = base.RestyClient.R()\n\t\tresult BaseRes\n\t)\n\n\tif host != \"\" {\n\t\treqUrl.Host = host\n\t}\n\treq.SetHeaders(map[string]string{\n\t\t\"Origin\": \"https://quqi.com\",\n\t\t\"Cookie\": d.Cookie,\n\t})\n\n\tif d.GroupID != \"\" {\n\t\treq.SetQueryParam(\"quqiid\", d.GroupID)\n\t}\n\n\tif callback != nil {\n\t\tcallback(req)\n\t}\n\n\tres, err := req.Execute(method, reqUrl.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// resty.Request.SetResult cannot parse result correctly sometimes\n\terr = utils.Json.Unmarshal(res.Body(), &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif result.Code != 0 {\n\t\treturn nil, errors.New(result.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, nil\n}\n\nfunc (d *Quqi) login() error {\n\tif d.Addition.Cookie != \"\" {\n\t\td.Cookie = d.Addition.Cookie\n\t}\n\tif d.checkLogin() {\n\t\treturn nil\n\t}\n\tif d.Cookie != \"\" {\n\t\treturn errors.New(\"cookie is invalid\")\n\t}\n\tif d.Phone == \"\" {\n\t\treturn errors.New(\"phone number is empty\")\n\t}\n\tif d.Password == \"\" {\n\t\treturn errs.EmptyPassword\n\t}\n\n\tresp, err := d.request(\"\", \"/auth/person/v2/login/password\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"phone\":    d.Phone,\n\t\t\t\"password\": base64.StdEncoding.EncodeToString([]byte(d.Password)),\n\t\t})\n\t}, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar cookies []string\n\tfor _, cookie := range resp.RawResponse.Cookies() {\n\t\tcookies = append(cookies, fmt.Sprintf(\"%s=%s\", cookie.Name, cookie.Value))\n\t}\n\td.Cookie = strings.Join(cookies, \";\")\n\n\treturn nil\n}\n\nfunc (d *Quqi) checkLogin() bool {\n\tif _, err := d.request(\"\", \"/auth/account/baseInfo\", resty.MethodGet, nil, nil); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// decryptKey 获取密码\nfunc decryptKey(encodeKey string) []byte {\n\t// 移除非法字符\n\tu := strings.ReplaceAll(encodeKey, \"[^A-Za-z0-9+\\\\/]\", \"\")\n\n\t// 计算输出字节数组的长度\n\to := len(u)\n\ta := 32\n\n\t// 创建输出字节数组\n\tc := make([]byte, a)\n\n\t// 编码循环\n\ts := uint32(0) // 累加器\n\tf := 0         // 输出数组索引\n\tfor l := 0; l < o; l++ {\n\t\tr := l & 3 // 取模4，得到当前字符在四字节块中的位置\n\t\ti := u[l]  // 当前字符的ASCII码\n\n\t\t// 编码当前字符\n\t\tswitch {\n\t\tcase i >= 65 && i < 91: // 大写字母\n\t\t\ts |= uint32(i-65) << uint32(6*(3-r))\n\t\tcase i >= 97 && i < 123: // 小写字母\n\t\t\ts |= uint32(i-71) << uint32(6*(3-r))\n\t\tcase i >= 48 && i < 58: // 数字\n\t\t\ts |= uint32(i+4) << uint32(6*(3-r))\n\t\tcase i == 43: // 加号\n\t\t\ts |= uint32(62) << uint32(6*(3-r))\n\t\tcase i == 47: // 斜杠\n\t\t\ts |= uint32(63) << uint32(6*(3-r))\n\t\t}\n\n\t\t// 如果累加器已经包含了四个字符，或者是最后一个字符，则写入输出数组\n\t\tif r == 3 || l == o-1 {\n\t\t\tfor e := 0; e < 3 && f < a; e, f = e+1, f+1 {\n\t\t\t\tc[f] = byte(s >> (16 >> e & 24) & 255)\n\t\t\t}\n\t\t\ts = 0\n\t\t}\n\t}\n\n\treturn c\n}\n\nfunc (d *Quqi) linkFromPreview(id string) (*model.Link, error) {\n\tvar getDocResp GetDocRes\n\tif _, err := d.request(\"\", \"/api/doc/getDoc\", resty.MethodPost, func(req *resty.Request) {\n\t\treq.SetFormData(map[string]string{\n\t\t\t\"quqi_id\":   d.GroupID,\n\t\t\t\"tree_id\":   \"1\",\n\t\t\t\"node_id\":   id,\n\t\t\t\"client_id\": d.ClientID,\n\t\t})\n\t}, &getDocResp); err != nil {\n\t\treturn nil, err\n\t}\n\tif getDocResp.Data.OriginPath == \"\" {\n\t\treturn nil, errors.New(\"cannot get link from preview\")\n\t}\n\treturn &model.Link{\n\t\tURL: getDocResp.Data.OriginPath,\n\t\tHeader: http.Header{\n\t\t\t\"Origin\": []string{\"https://quqi.com\"},\n\t\t\t\"Cookie\": []string{d.Cookie},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Quqi) linkFromDownload(id string) (*model.Link, error) {\n\tvar getDownloadResp GetDownloadResp\n\tif _, err := d.request(\"\", \"/api/doc/getDownload\", resty.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"quqi_id\":     d.GroupID,\n\t\t\t\"tree_id\":     \"1\",\n\t\t\t\"node_id\":     id,\n\t\t\t\"url_type\":    \"undefined\",\n\t\t\t\"entry_type\":  \"undefined\",\n\t\t\t\"client_id\":   d.ClientID,\n\t\t\t\"no_redirect\": \"1\",\n\t\t})\n\t}, &getDownloadResp); err != nil {\n\t\treturn nil, err\n\t}\n\tif getDownloadResp.Data.Url == \"\" {\n\t\treturn nil, errors.New(\"cannot get link from download\")\n\t}\n\n\treturn &model.Link{\n\t\tURL: getDownloadResp.Data.Url,\n\t\tHeader: http.Header{\n\t\t\t\"Origin\": []string{\"https://quqi.com\"},\n\t\t\t\"Cookie\": []string{d.Cookie},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Quqi) linkFromCDN(id string) (*model.Link, error) {\n\tdownloadLink, err := d.linkFromDownload(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar urlExchangeResp UrlExchangeResp\n\tif _, err = d.request(\"api.quqi.com\", \"/preview/downloadInfo/url/exchange\", resty.MethodGet, func(req *resty.Request) {\n\t\treq.SetQueryParam(\"url\", downloadLink.URL)\n\t}, &urlExchangeResp); err != nil {\n\t\treturn nil, err\n\t}\n\tif urlExchangeResp.Data.Url == \"\" {\n\t\treturn nil, errors.New(\"cannot get link from cdn\")\n\t}\n\n\t// 假设存在未加密的情况\n\tif !urlExchangeResp.Data.IsEncrypted {\n\t\treturn &model.Link{\n\t\t\tURL: urlExchangeResp.Data.Url,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Origin\": []string{\"https://quqi.com\"},\n\t\t\t\t\"Cookie\": []string{d.Cookie},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// 根据sio(https://github.com/minio/sio/blob/master/DARE.md)描述及实际测试，得出以下结论：\n\t// 1. 加密后大小(encrypted_size)-原始文件大小(size) = 加密包的头大小+身份验证标识 = (16+16) * N  ->  N为加密包的数量\n\t// 2. 原始文件大小(size)+64*1024-1 / (64*1024) = N  ->  每个包的有效负载为64K\n\tremoteClosers := utils.EmptyClosers()\n\tpayloadSize := int64(1 << 16)\n\texpiration := time.Until(time.Unix(urlExchangeResp.Data.ExpiredTime, 0))\n\tresultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\tencryptedOffset := httpRange.Start / payloadSize * (payloadSize + 32)\n\t\tdecryptedOffset := httpRange.Start % payloadSize\n\t\tencryptedLength := (httpRange.Length+httpRange.Start+payloadSize-1)/payloadSize*(payloadSize+32) - encryptedOffset\n\t\tif httpRange.Length < 0 {\n\t\t\tencryptedLength = httpRange.Length\n\t\t} else {\n\t\t\tif httpRange.Length+httpRange.Start >= urlExchangeResp.Data.Size || encryptedLength+encryptedOffset >= urlExchangeResp.Data.EncryptedSize {\n\t\t\t\tencryptedLength = -1\n\t\t\t}\n\t\t}\n\t\t//log.Debugf(\"size: %d\\tencrypted_size: %d\", urlExchangeResp.Data.Size, urlExchangeResp.Data.EncryptedSize)\n\t\t//log.Debugf(\"http range offset: %d, length: %d\", httpRange.Start, httpRange.Length)\n\t\t//log.Debugf(\"encrypted offset: %d, length: %d, decrypted offset: %d\", encryptedOffset, encryptedLength, decryptedOffset)\n\n\t\trrc, err := stream.GetRangeReadCloserFromLink(urlExchangeResp.Data.EncryptedSize, &model.Link{\n\t\t\tURL: urlExchangeResp.Data.Url,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Origin\": []string{\"https://quqi.com\"},\n\t\t\t\t\"Cookie\": []string{d.Cookie},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\trc, err := rrc.RangeRead(ctx, http_range.Range{Start: encryptedOffset, Length: encryptedLength})\n\t\tremoteClosers.AddClosers(rrc.GetClosers())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdecryptReader, err := sio.DecryptReader(rc, sio.Config{\n\t\t\tMinVersion:     sio.Version10,\n\t\t\tMaxVersion:     sio.Version20,\n\t\t\tCipherSuites:   []byte{sio.CHACHA20_POLY1305, sio.AES_256_GCM},\n\t\t\tKey:            decryptKey(urlExchangeResp.Data.EncryptedKey),\n\t\t\tSequenceNumber: uint32(httpRange.Start / payloadSize),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbufferReader := bufio.NewReader(decryptReader)\n\t\tbufferReader.Discard(int(decryptedOffset))\n\n\t\treturn io.NopCloser(bufferReader), nil\n\t}\n\n\treturn &model.Link{\n\t\tRangeReadCloser: &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers},\n\t\tExpiration:      &expiration,\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(\"POST\", \"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\"io\"\n\t\"net/url\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/aws/aws-sdk-go/aws\"\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\n\tconfig driver.Config\n\tcron   *cron.Cron\n}\n\nvar storageClassLookup = map[string]string{\n\t\"standard\":            s3.ObjectStorageClassStandard,\n\t\"reduced_redundancy\":  s3.ObjectStorageClassReducedRedundancy,\n\t\"glacier\":             s3.ObjectStorageClassGlacier,\n\t\"standard_ia\":         s3.ObjectStorageClassStandardIa,\n\t\"onezone_ia\":          s3.ObjectStorageClassOnezoneIa,\n\t\"intelligent_tiering\": s3.ObjectStorageClassIntelligentTiering,\n\t\"deep_archive\":        s3.ObjectStorageClassDeepArchive,\n\t\"outposts\":            s3.ObjectStorageClassOutposts,\n\t\"glacier_ir\":          s3.ObjectStorageClassGlacierIr,\n\t\"snow\":                s3.ObjectStorageClassSnow,\n\t\"express_onezone\":     s3.ObjectStorageClassExpressOnezone,\n}\n\nfunc (d *S3) resolveStorageClass() *string {\n\tvalue := strings.TrimSpace(d.StorageClass)\n\tif value == \"\" {\n\t\treturn nil\n\t}\n\tnormalized := strings.ToLower(strings.ReplaceAll(value, \"-\", \"_\"))\n\tif v, ok := storageClassLookup[normalized]; ok {\n\t\treturn aws.String(v)\n\t}\n\tlog.Warnf(\"s3: unknown storage class %q, using raw value\", d.StorageClass)\n\treturn aws.String(value)\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 = \"alist\"\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(false)\n\t\t\td.linkClient = d.getClient(true)\n\t\t})\n\t}\n\terr := d.initSession()\n\tif err != nil {\n\t\treturn err\n\t}\n\td.client = d.getClient(false)\n\td.linkClient = d.getClient(true)\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\tdisposition := fmt.Sprintf(`attachment; filename*=UTF-8''%s`, url.PathEscape(filename))\n\tif d.AddFilenameToDisposition {\n\t\tdisposition = fmt.Sprintf(`attachment; filename=\"%s\"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))\n\t}\n\tinput := &s3.GetObjectInput{\n\t\tBucket: &d.Bucket,\n\t\tKey:    &path,\n\t\t//ResponseContentDisposition: &disposition,\n\t}\n\tif d.CustomHost == \"\" {\n\t\tinput.ResponseContentDisposition = &disposition\n\t}\n\treq, _ := d.linkClient.GetObjectRequest(input)\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 d.RemoveBucket {\n\t\t\tlink.URL = strings.Replace(link.URL, \"/\"+d.Bucket, \"\", 1)\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:   io.NopCloser(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\tif storageClass := d.resolveStorageClass(); storageClass != nil {\n\t\tinput.StorageClass = storageClass\n\t}\n\t_, err := uploader.UploadWithContext(ctx, input)\n\treturn err\n}\n\nvar (\n\t_ driver.Driver = (*S3)(nil)\n\t_ driver.Other  = (*S3)(nil)\n)\n"
  },
  {
    "path": "drivers/s3/meta.go",
    "content": "package s3\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tStorageClass             string `json:\"storage_class\" type:\"select\" options:\",standard,standard_ia,onezone_ia,intelligent_tiering,glacier,glacier_ir,deep_archive,archive\" help:\"Storage class for new objects. AWS and Tencent COS support different subsets (COS uses ARCHIVE/DEEP_ARCHIVE).\"`\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/other.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/service/s3\"\n)\n\nconst (\n\tOtherMethodArchive       = \"archive\"\n\tOtherMethodArchiveStatus = \"archive_status\"\n\tOtherMethodThaw          = \"thaw\"\n\tOtherMethodThawStatus    = \"thaw_status\"\n)\n\ntype ArchiveRequest struct {\n\tStorageClass string `json:\"storage_class\"`\n}\n\ntype ThawRequest struct {\n\tDays int64  `json:\"days\"`\n\tTier string `json:\"tier\"`\n}\n\ntype ObjectDescriptor struct {\n\tPath   string `json:\"path\"`\n\tBucket string `json:\"bucket\"`\n\tKey    string `json:\"key\"`\n}\n\ntype ArchiveResponse struct {\n\tAction       string           `json:\"action\"`\n\tObject       ObjectDescriptor `json:\"object\"`\n\tStorageClass string           `json:\"storage_class\"`\n\tRequestID    string           `json:\"request_id,omitempty\"`\n\tVersionID    string           `json:\"version_id,omitempty\"`\n\tETag         string           `json:\"etag,omitempty\"`\n\tLastModified string           `json:\"last_modified,omitempty\"`\n}\n\ntype ThawResponse struct {\n\tAction    string           `json:\"action\"`\n\tObject    ObjectDescriptor `json:\"object\"`\n\tRequestID string           `json:\"request_id,omitempty\"`\n\tStatus    *RestoreStatus   `json:\"status,omitempty\"`\n}\n\ntype RestoreStatus struct {\n\tOngoing bool   `json:\"ongoing\"`\n\tExpiry  string `json:\"expiry,omitempty\"`\n\tRaw     string `json:\"raw\"`\n}\n\nfunc (d *S3) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tif args.Obj == nil {\n\t\treturn nil, fmt.Errorf(\"missing object reference\")\n\t}\n\tif args.Obj.IsDir() {\n\t\treturn nil, errs.NotSupport\n\t}\n\n\tswitch strings.ToLower(strings.TrimSpace(args.Method)) {\n\tcase \"archive\":\n\t\treturn d.archive(ctx, args)\n\tcase \"archive_status\":\n\t\treturn d.archiveStatus(ctx, args)\n\tcase \"thaw\":\n\t\treturn d.thaw(ctx, args)\n\tcase \"thaw_status\":\n\t\treturn d.thawStatus(ctx, args)\n\tdefault:\n\t\treturn nil, errs.NotSupport\n\t}\n}\n\nfunc (d *S3) archive(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tkey := getKey(args.Obj.GetPath(), false)\n\tpayload := ArchiveRequest{}\n\tif err := DecodeOtherArgs(args.Data, &payload); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse archive request: %w\", err)\n\t}\n\tif payload.StorageClass == \"\" {\n\t\treturn nil, fmt.Errorf(\"storage_class is required\")\n\t}\n\tstorageClass := NormalizeStorageClass(payload.StorageClass)\n\tinput := &s3.CopyObjectInput{\n\t\tBucket:            &d.Bucket,\n\t\tKey:               &key,\n\t\tCopySource:        aws.String(url.PathEscape(d.Bucket + \"/\" + key)),\n\t\tMetadataDirective: aws.String(s3.MetadataDirectiveCopy),\n\t\tStorageClass:      aws.String(storageClass),\n\t}\n\tcopyReq, output := d.client.CopyObjectRequest(input)\n\tcopyReq.SetContext(ctx)\n\tif err := copyReq.Send(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := ArchiveResponse{\n\t\tAction:       \"archive\",\n\t\tObject:       d.describeObject(args.Obj, key),\n\t\tStorageClass: storageClass,\n\t\tRequestID:    copyReq.RequestID,\n\t}\n\tif output.VersionId != nil {\n\t\tresp.VersionID = aws.StringValue(output.VersionId)\n\t}\n\tif result := output.CopyObjectResult; result != nil {\n\t\tresp.ETag = aws.StringValue(result.ETag)\n\t\tif result.LastModified != nil {\n\t\t\tresp.LastModified = result.LastModified.UTC().Format(time.RFC3339)\n\t\t}\n\t}\n\tif status, err := d.describeObjectStatus(ctx, key); err == nil {\n\t\tif status.StorageClass != \"\" {\n\t\t\tresp.StorageClass = status.StorageClass\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (d *S3) archiveStatus(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tkey := getKey(args.Obj.GetPath(), false)\n\tstatus, err := d.describeObjectStatus(ctx, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ArchiveResponse{\n\t\tAction:       \"archive_status\",\n\t\tObject:       d.describeObject(args.Obj, key),\n\t\tStorageClass: status.StorageClass,\n\t}, nil\n}\n\nfunc (d *S3) thaw(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tkey := getKey(args.Obj.GetPath(), false)\n\tpayload := ThawRequest{Days: 1}\n\tif err := DecodeOtherArgs(args.Data, &payload); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse thaw request: %w\", err)\n\t}\n\tif payload.Days <= 0 {\n\t\tpayload.Days = 1\n\t}\n\trestoreRequest := &s3.RestoreRequest{\n\t\tDays: aws.Int64(payload.Days),\n\t}\n\tif tier := NormalizeRestoreTier(payload.Tier); tier != \"\" {\n\t\trestoreRequest.GlacierJobParameters = &s3.GlacierJobParameters{Tier: aws.String(tier)}\n\t}\n\tinput := &s3.RestoreObjectInput{\n\t\tBucket:         &d.Bucket,\n\t\tKey:            &key,\n\t\tRestoreRequest: restoreRequest,\n\t}\n\trestoreReq, _ := d.client.RestoreObjectRequest(input)\n\trestoreReq.SetContext(ctx)\n\tif err := restoreReq.Send(); err != nil {\n\t\treturn nil, err\n\t}\n\tstatus, _ := d.describeObjectStatus(ctx, key)\n\tresp := ThawResponse{\n\t\tAction:    \"thaw\",\n\t\tObject:    d.describeObject(args.Obj, key),\n\t\tRequestID: restoreReq.RequestID,\n\t}\n\tif status != nil {\n\t\tresp.Status = status.Restore\n\t}\n\treturn resp, nil\n}\n\nfunc (d *S3) thawStatus(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n\tkey := getKey(args.Obj.GetPath(), false)\n\tstatus, err := d.describeObjectStatus(ctx, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ThawResponse{\n\t\tAction: \"thaw_status\",\n\t\tObject: d.describeObject(args.Obj, key),\n\t\tStatus: status.Restore,\n\t}, nil\n}\n\nfunc (d *S3) describeObject(obj model.Obj, key string) ObjectDescriptor {\n\treturn ObjectDescriptor{\n\t\tPath:   obj.GetPath(),\n\t\tBucket: d.Bucket,\n\t\tKey:    key,\n\t}\n}\n\ntype objectStatus struct {\n\tStorageClass string\n\tRestore      *RestoreStatus\n}\n\nfunc (d *S3) describeObjectStatus(ctx context.Context, key string) (*objectStatus, error) {\n\thead, err := d.client.HeadObjectWithContext(ctx, &s3.HeadObjectInput{Bucket: &d.Bucket, Key: &key})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstatus := &objectStatus{\n\t\tStorageClass: aws.StringValue(head.StorageClass),\n\t\tRestore:      parseRestoreHeader(head.Restore),\n\t}\n\treturn status, nil\n}\n\nfunc parseRestoreHeader(header *string) *RestoreStatus {\n\tif header == nil {\n\t\treturn nil\n\t}\n\tvalue := strings.TrimSpace(*header)\n\tif value == \"\" {\n\t\treturn nil\n\t}\n\tstatus := &RestoreStatus{Raw: value}\n\tparts := strings.Split(value, \",\")\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(part, \"ongoing-request=\") {\n\t\t\tstatus.Ongoing = strings.Contains(part, \"\\\"true\\\"\")\n\t\t}\n\t\tif strings.HasPrefix(part, \"expiry-date=\") {\n\t\t\texpiry := strings.Trim(part[len(\"expiry-date=\"):], \"\\\"\")\n\t\t\tif expiry != \"\" {\n\t\t\t\tif t, err := time.Parse(time.RFC1123, expiry); err == nil {\n\t\t\t\t\tstatus.Expiry = t.UTC().Format(time.RFC3339)\n\t\t\t\t} else {\n\t\t\t\t\tstatus.Expiry = expiry\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn status\n}\n\nfunc DecodeOtherArgs(data interface{}, target interface{}) error {\n\tif data == nil {\n\t\treturn nil\n\t}\n\traw, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn json.Unmarshal(raw, target)\n}\n\nfunc NormalizeStorageClass(value string) string {\n\tnormalized := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(value, \"-\", \"_\")))\n\tif normalized == \"\" {\n\t\treturn value\n\t}\n\tif v, ok := storageClassLookup[normalized]; ok {\n\t\treturn v\n\t}\n\treturn value\n}\n\nfunc NormalizeRestoreTier(value string) string {\n\tnormalized := strings.ToLower(strings.TrimSpace(value))\n\tswitch normalized {\n\tcase \"\", \"default\":\n\t\treturn \"\"\n\tcase \"bulk\":\n\t\treturn s3.TierBulk\n\tcase \"standard\":\n\t\treturn s3.TierStandard\n\tcase \"expedited\":\n\t\treturn s3.TierExpedited\n\tdefault:\n\t\treturn value\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\nfunc (d *S3) getClient(link bool) *s3.S3 {\n\tclient := s3.New(d.Session)\n\tif link && 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\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 = \".alist\"\n\nfunc getPlaceholderName(placeholder string) string {\n\tif placeholder == \"\" {\n\t\treturn defaultPlaceholderName\n\t}\n\treturn placeholder\n}\n\nfunc (d *S3) listV1(prefix string, args model.ListArgs) ([]model.Obj, error) {\n\tprefix = getKey(prefix, 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\t//Id:        *object.Key,\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\t//Id:        *object.Key,\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, model.WrapObjStorageClass(file, aws.StringValue(object.StorageClass)))\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(prefix string, args model.ListArgs) ([]model.Obj, error) {\n\tprefix = getKey(prefix, 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\t//Id:        *object.Key,\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\t//Id:        *object.Key,\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, model.WrapObjStorageClass(file, aws.StringValue(object.StorageClass)))\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\tinput := &s3.CopyObjectInput{\n\t\tBucket:     &d.Bucket,\n\t\tCopySource: aws.String(url.PathEscape(d.Bucket + \"/\" + srcKey)),\n\t\tKey:        &dstKey,\n\t}\n\tif storageClass := d.resolveStorageClass(); storageClass != nil {\n\t\tinput.StorageClass = storageClass\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\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tlibraryMap    map[string]*LibraryInfo\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\td.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)\n\td.libraryMap = make(map[string]*LibraryInfo)\n\treturn d.getToken()\n}\n\nfunc (d *Seafile) Drop(ctx context.Context) error {\n\treturn 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\tif path == d.RootFolderPath {\n\t\tlibraries, err := d.listLibraries()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif path == \"/\" && d.RepoId == \"\" {\n\t\t\treturn utils.SliceConvert(libraries, func(f LibraryItemResp) (model.Obj, error) {\n\t\t\t\treturn &model.Object{\n\t\t\t\t\tName:     f.Name,\n\t\t\t\t\tModified: time.Unix(f.Modified, 0),\n\t\t\t\t\tSize:     f.Size,\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t}, nil\n\t\t\t})\n\t\t}\n\t}\n\tvar repo *LibraryInfo\n\trepo, path, err = d.getRepoAndPath(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif repo.Encrypted {\n\t\terr = d.decryptLibrary(repo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tvar resp []RepoDirItemResp\n\t_, err = d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/dir/\", repo.Id), 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 RepoDirItemResp) (model.Obj, error) {\n\t\treturn &model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name,\n\t\t\t\tModified: time.Unix(f.Modified, 0),\n\t\t\t\tSize:     f.Size,\n\t\t\t\tIsFolder: f.Type == \"dir\",\n\t\t\t},\n\t\t\t// Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},\n\t\t}, nil\n\t})\n}\n\nfunc (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\trepo, path, err := d.getRepoAndPath(file.GetPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres, err := d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/file/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\":     path,\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\trepo, path, err := d.getRepoAndPath(parentDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath, _ = utils.JoinBasePath(path, dirName)\n\t_, err = d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/dir/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\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\trepo, path, err := d.getRepoAndPath(srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"move\",\n\t\t\t\"dst_repo\":  dstRepo.Id,\n\t\t\t\"dst_dir\":   dstPath,\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\trepo, path, err := d.getRepoAndPath(srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\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\trepo, path, err := d.getRepoAndPath(srcObj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = d.request(http.MethodPost, fmt.Sprintf(\"/api2/repos/%s/file/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\n\t\t}).SetFormData(map[string]string{\n\t\t\t\"operation\": \"copy\",\n\t\t\t\"dst_repo\":  dstRepo.Id,\n\t\t\t\"dst_dir\":   dstPath,\n\t\t})\n\t})\n\treturn err\n}\n\nfunc (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {\n\trepo, path, err := d.getRepoAndPath(obj.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = d.request(http.MethodDelete, fmt.Sprintf(\"/api2/repos/%s/file/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\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\trepo, path, err := d.getRepoAndPath(dstDir.GetPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tres, err := d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/upload-link/\", repo.Id), func(req *resty.Request) {\n\t\treq.SetQueryParams(map[string]string{\n\t\t\t\"p\": path,\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\": path,\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\"`\t\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 \"time\"\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\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 RepoDirItemResp struct {\n\tRepoItemResp\n}\n\ntype LibraryInfo struct {\n\tLibraryItemResp\n\tdecryptedTime    time.Time\n\tdecryptedSuccess bool\n}"
  },
  {
    "path": "drivers/seafile/util.go",
    "content": "package seafile\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/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) getRepoAndPath(fullPath string) (repo *LibraryInfo, path string, err error) {\n\tlibraryMap := d.libraryMap\n\trepoId := d.Addition.RepoId\n\tif repoId != \"\" {\n\t\tif len(repoId) == 36 /* uuid */ {\n\t\t\tfor _, library := range libraryMap {\n\t\t\t\tif library.Id == repoId {\n\t\t\t\t\treturn library, fullPath, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tvar repoName string\n\t\tstr := fullPath[1:]\n\t\tpos := strings.IndexRune(str, '/')\n\t\tif pos == -1 {\n\t\t\trepoName = str\n\t\t} else {\n\t\t\trepoName = str[:pos]\n\t\t}\n\t\tpath = utils.FixAndCleanPath(fullPath[1+len(repoName):])\n\t\tif library, ok := libraryMap[repoName]; ok {\n\t\t\treturn library, path, nil\n\t\t}\n\t}\n\treturn nil, \"\", errs.ObjectNotFound\n}\n\nfunc (d *Seafile) listLibraries() (resp []LibraryItemResp, err error) {\n\trepoId := d.Addition.RepoId\n\tif repoId == \"\" {\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} else {\n\t\tvar oneResp LibraryItemResp\n\t\t_, err = d.request(http.MethodGet, fmt.Sprintf(\"/api2/repos/%s/\", repoId), func(req *resty.Request) {\n\t\t\treq.SetResult(&oneResp)\n\t\t})\n\t\tif err == nil {\n\t\t\tresp = append(resp, oneResp)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlibraryMap := make(map[string]*LibraryInfo)\n\tvar putLibraryMap func(library LibraryItemResp, index int)\n\tputLibraryMap = func(library LibraryItemResp, index int) {\n\t\tname := library.Name\n\t\tif index > 0 {\n\t\t\tname = fmt.Sprintf(\"%s (%d)\", name, index)\n\t\t}\n\t\tif _, exist := libraryMap[name]; exist {\n\t\t\tputLibraryMap(library, index+1)\n\t\t} else {\n\t\t\tlibraryInfo := LibraryInfo{}\n\t\t\tdata, _ := utils.Json.Marshal(library)\n\t\t\t_ = utils.Json.Unmarshal(data, &libraryInfo)\n\t\t\tlibraryMap[name] = &libraryInfo\n\t\t}\n\t}\n\tfor _, library := range resp {\n\t\tputLibraryMap(library, 0)\n\t}\n\td.libraryMap = libraryMap\n\treturn resp, nil\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\n\n"
  },
  {
    "path": "drivers/sftp/driver.go",
    "content": "package sftp\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tlink := &model.Link{\n\t\tMFile: remoteFile,\n\t}\n\treturn link, 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\nvar _ driver.Driver = (*SFTP)(nil)\n"
  },
  {
    "path": "drivers/sftp/meta.go",
    "content": "package sftp\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:   true,\n\tDefaultRoot: \"/\",\n\tCheckStatus: 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/alist-org/alist/v3/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\tif !symlink {\n\t\treturn &model.Object{\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\tpath := stdpath.Join(dir, f.Name())\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\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\"path\"\n\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\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\t_ = d.client.Close()\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/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\n\t\"github.com/hirochachacha/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.Index(d.Addition.Address, \":\") < 0 {\n\t\td.Addition.Address = d.Addition.Address + \":445\"\n\t}\n\treturn d.initFS()\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(); 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\tvar files []model.Obj\n\tfor _, f := range rawFiles {\n\t\tfile := model.ObjThumb{\n\t\t\tObject: model.Object{\n\t\t\t\tName:     f.Name(),\n\t\t\t\tModified: f.ModTime(),\n\t\t\t\tSize:     f.Size(),\n\t\t\t\tIsFolder: f.IsDir(),\n\t\t\t\tCtime:    f.(*smb2.FileStat).CreationTime,\n\t\t\t},\n\t\t}\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(); 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\tlink := &model.Link{\n\t\tMFile: remoteFile,\n\t}\n\td.updateLastConnTime()\n\treturn link, nil\n}\n\nfunc (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tif err := d.checkConn(); 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(); 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(); 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(); 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(); 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(); 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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:   true,\n\tDefaultRoot: \".\",\n\tNoCache:     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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/hirochachacha/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() error {\n\tconn, err := net.Dial(\"tcp\", d.Address)\n\tif err != nil {\n\t\treturn err\n\t}\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(conn)\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() 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()\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/streamtape/driver.go",
    "content": "package streamtape\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Streamtape struct {\n\tmodel.Storage\n\tAddition\n}\n\nvar waitMoreSecondsRe = regexp.MustCompile(`wait\\s+(\\d+)\\s+more\\s+seconds?`)\n\nfunc (d *Streamtape) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Streamtape) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Streamtape) Init(ctx context.Context) error {\n\tif strings.TrimSpace(d.APILogin) == \"\" || strings.TrimSpace(d.APIKey) == \"\" {\n\t\treturn errors.New(\"api_login and api_key are required\")\n\t}\n\tif d.RootFolderID == \"\" {\n\t\td.RootFolderID = \"0\"\n\t}\n\n\tvar account accountInfo\n\tif err := d.callAPI(ctx, \"/account/info\", nil, &account); err != nil {\n\t\treturn err\n\t}\n\n\top.MustSaveDriverStorage(d)\n\treturn nil\n}\n\nfunc (d *Streamtape) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Streamtape) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfolderID := d.RootFolderID\n\tif dir.GetID() != \"\" {\n\t\tfolderID = folderIDFromObjID(dir.GetID())\n\t}\n\n\tparams := map[string]string{}\n\tif folderID != \"\" && folderID != \"0\" {\n\t\tparams[\"folder\"] = folderID\n\t}\n\n\tvar result listFolderResult\n\tif err := d.callAPI(ctx, \"/file/listfolder\", params, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tobjects := make([]model.Obj, 0, len(result.Folders)+len(result.Files))\n\tfor _, f := range result.Folders {\n\t\tobjects = append(objects, &model.Object{\n\t\t\tID:       encodeFolderID(f.ID),\n\t\t\tName:     f.Name,\n\t\t\tIsFolder: true,\n\t\t})\n\t}\n\tfor _, f := range result.Files {\n\t\tobjects = append(objects, buildFileObj(f))\n\t}\n\treturn objects, nil\n}\n\nfunc (d *Streamtape) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.IsDir() {\n\t\treturn nil, errs.NotFile\n\t}\n\tfileID := fileIDFromObjID(file.GetID())\n\tif fileID == \"\" {\n\t\treturn nil, errors.New(\"empty file id\")\n\t}\n\n\tvar ticket dlTicketResult\n\tif err := d.callAPI(ctx, \"/file/dlticket\", map[string]string{\"file\": fileID}, &ticket); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar dl dlResult\n\twaitSeconds := ticket.WaitTime\n\tif waitSeconds > 0 {\n\t\ttimer := time.NewTimer(time.Duration(waitSeconds+1) * time.Second)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\ttimer.Stop()\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-timer.C:\n\t\t}\n\t}\n\n\tvar err error\n\tfor i := 0; i < 3; i++ {\n\t\terr = d.callAPI(ctx, \"/file/dl\", map[string]string{\n\t\t\t\"file\":   fileID,\n\t\t\t\"ticket\": ticket.Ticket,\n\t\t}, &dl)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\twaitSeconds = extractWaitSecondsFromErr(err)\n\t\tif waitSeconds <= 0 {\n\t\t\treturn nil, err\n\t\t}\n\t\ttimer := time.NewTimer(time.Duration(waitSeconds+1) * time.Second)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\ttimer.Stop()\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-timer.C:\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfinalURL := ensureStreamQuery(dl.URL)\n\tlog.Infof(\"streamtape direct link file=%s url=%s\", fileID, finalURL)\n\tlink := &model.Link{\n\t\tURL: finalURL,\n\t\tHeader: http.Header{\n\t\t\t\"Referer\": []string{\"https://streamtape.com/\"},\n\t\t\t\"Origin\":  []string{\"https://streamtape.com\"},\n\t\t},\n\t}\n\td.applyRangeStrategy(link, file.GetSize())\n\treturn link, nil\n}\n\nfunc extractWaitSecondsFromErr(err error) int {\n\tif err == nil {\n\t\treturn 0\n\t}\n\tmatches := waitMoreSecondsRe.FindStringSubmatch(strings.ToLower(err.Error()))\n\tif len(matches) < 2 {\n\t\treturn 0\n\t}\n\tseconds, convErr := strconv.Atoi(matches[1])\n\tif convErr != nil || seconds < 0 {\n\t\treturn 0\n\t}\n\treturn seconds\n}\n\nfunc ensureStreamQuery(rawURL string) string {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn rawURL\n\t}\n\tq := u.Query()\n\tif q.Get(\"stream\") == \"\" {\n\t\tq.Set(\"stream\", \"1\")\n\t\tu.RawQuery = q.Encode()\n\t}\n\treturn u.String()\n}\n\nfunc (d *Streamtape) applyRangeStrategy(link *model.Link, size int64) {\n\tif !d.EnableRangeControl || size <= 0 {\n\t\treturn\n\t}\n\n\tmode := strings.ToLower(strings.TrimSpace(d.RangeMode))\n\tif mode == \"\" {\n\t\tmode = \"chunk\"\n\t}\n\n\tswitch mode {\n\tcase \"full\":\n\t\t// Keep single full-tail behavior while still using ranged requests.\n\t\tlink.Concurrency = 1\n\t\tlink.PartSize = int(size)\n\tcase \"percent\":\n\t\tpercent := d.RangePercent\n\t\tif percent <= 0 {\n\t\t\tpercent = 15\n\t\t}\n\t\tif percent > 100 {\n\t\t\tpercent = 100\n\t\t}\n\t\tpartSize := size * int64(percent) / 100\n\t\tif partSize < 1*1024*1024 {\n\t\t\tpartSize = 1 * 1024 * 1024\n\t\t}\n\t\tif partSize > size {\n\t\t\tpartSize = size\n\t\t}\n\t\tlink.Concurrency = 1\n\t\tlink.PartSize = int(partSize)\n\tdefault:\n\t\tchunkMB := d.RangeChunkMB\n\t\tif chunkMB <= 0 {\n\t\t\tchunkMB = 8\n\t\t}\n\t\tpartSize := int64(chunkMB) * 1024 * 1024\n\t\tif partSize > size {\n\t\t\tpartSize = size\n\t\t}\n\t\tconcurrency := d.RangeConcurrency\n\t\tif concurrency <= 0 {\n\t\t\tconcurrency = 4\n\t\t}\n\t\tlink.Concurrency = concurrency\n\t\tlink.PartSize = int(partSize)\n\t}\n}\n\nfunc (d *Streamtape) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {\n\tpid := d.RootFolderID\n\tif parentDir.GetID() != \"\" {\n\t\tpid = folderIDFromObjID(parentDir.GetID())\n\t}\n\n\tparams := map[string]string{\"name\": dirName}\n\tif pid != \"\" && pid != \"0\" {\n\t\tparams[\"pid\"] = pid\n\t}\n\n\tvar result createFolderResult\n\tif err := d.callAPI(ctx, \"/file/createfolder\", params, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       encodeFolderID(result.FolderID),\n\t\tName:     dirName,\n\t\tIsFolder: true,\n\t}, nil\n}\n\nfunc (d *Streamtape) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\tif srcObj.IsDir() {\n\t\treturn nil, errs.NotImplement\n\t}\n\tfileID := fileIDFromObjID(srcObj.GetID())\n\tif fileID == \"\" {\n\t\treturn nil, errors.New(\"empty file id\")\n\t}\n\tfolderID := d.RootFolderID\n\tif dstDir.GetID() != \"\" {\n\t\tfolderID = folderIDFromObjID(dstDir.GetID())\n\t}\n\tif folderID == \"\" || folderID == \"0\" {\n\t\treturn nil, fmt.Errorf(\"streamtape move to root is not supported by API\")\n\t}\n\n\tif err := d.callAPI(ctx, \"/file/move\", map[string]string{\n\t\t\"file\":   fileID,\n\t\t\"folder\": folderID,\n\t}, nil); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &model.Object{\n\t\tID:       srcObj.GetID(),\n\t\tName:     srcObj.GetName(),\n\t\tSize:     srcObj.GetSize(),\n\t\tModified: srcObj.ModTime(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\nfunc (d *Streamtape) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {\n\tendpoint := \"/file/rename\"\n\tparams := map[string]string{\"name\": newName}\n\tif srcObj.IsDir() {\n\t\tendpoint = \"/file/renamefolder\"\n\t\tparams[\"folder\"] = folderIDFromObjID(srcObj.GetID())\n\t} else {\n\t\tparams[\"file\"] = fileIDFromObjID(srcObj.GetID())\n\t}\n\n\tif err := d.callAPI(ctx, endpoint, params, nil); 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\tIsFolder: srcObj.IsDir(),\n\t}, nil\n}\n\nfunc (d *Streamtape) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Streamtape) Remove(ctx context.Context, obj model.Obj) error {\n\tendpoint := \"/file/delete\"\n\tparams := map[string]string{}\n\tif obj.IsDir() {\n\t\tendpoint = \"/file/deletefolder\"\n\t\tparams[\"folder\"] = folderIDFromObjID(obj.GetID())\n\t} else {\n\t\tparams[\"file\"] = fileIDFromObjID(obj.GetID())\n\t}\n\treturn d.callAPI(ctx, endpoint, params, nil)\n}\n\nfunc (d *Streamtape) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {\n\tfolderID := d.RootFolderID\n\tif dstDir.GetID() != \"\" {\n\t\tfolderID = folderIDFromObjID(dstDir.GetID())\n\t}\n\n\tparams := map[string]string{}\n\tif folderID != \"\" && folderID != \"0\" {\n\t\tparams[\"folder\"] = folderID\n\t}\n\n\tvar uploadURL uploadURLResult\n\tif err := d.callAPI(ctx, \"/file/ul\", params, &uploadURL); err != nil {\n\t\treturn nil, err\n\t}\n\n\treader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         file,\n\t\tUpdateProgress: up,\n\t})\n\n\tres, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetFileReader(\"file1\", file.GetName(), reader).\n\t\tPost(uploadURL.URL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode() >= http.StatusBadRequest {\n\t\treturn nil, fmt.Errorf(\"streamtape upload failed: http %d\", res.StatusCode())\n\t}\n\n\tuploadedID := extractFileIDFromUploadBody(res.Body())\n\tif uploadedID == \"\" {\n\t\tlist, listErr := d.List(ctx, &model.Object{ID: encodeFolderID(folderID), IsFolder: true}, model.ListArgs{})\n\t\tif listErr == nil {\n\t\t\tfor _, obj := range list {\n\t\t\t\tif obj.IsDir() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif obj.GetName() == file.GetName() && (file.GetSize() <= 0 || obj.GetSize() == file.GetSize()) {\n\t\t\t\t\treturn obj, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &model.Object{\n\t\t\tName:     file.GetName(),\n\t\t\tSize:     file.GetSize(),\n\t\t\tIsFolder: false,\n\t\t}, nil\n\t}\n\n\treturn &model.Object{\n\t\tID:       encodeFileID(uploadedID),\n\t\tName:     file.GetName(),\n\t\tSize:     file.GetSize(),\n\t\tIsFolder: false,\n\t}, nil\n}\n\nfunc (d *Streamtape) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Streamtape) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Streamtape) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {\n\treturn nil, errs.NotImplement\n}\n\nfunc (d *Streamtape) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {\n\treturn nil, errs.NotImplement\n}\n\nvar _ driver.Driver = (*Streamtape)(nil)\n"
  },
  {
    "path": "drivers/streamtape/meta.go",
    "content": "package streamtape\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tAPILogin           string `json:\"api_login\" required:\"true\" help:\"API Login from Streamtape account settings\"`\n\tAPIKey             string `json:\"api_key\" required:\"true\" help:\"API Key from Streamtape account settings\"`\n\tRangeMode          string `json:\"range_mode\" type:\"select\" options:\"chunk,full,percent\" default:\"chunk\" help:\"Range strategy for preview: chunk=bounded ranges, full=single full-tail range, percent=part size by file percentage\"`\n\tRangeChunkMB       int    `json:\"range_chunk_mb\" type:\"number\" default:\"8\" help:\"Chunk mode part size in MB\"`\n\tRangeConcurrency   int    `json:\"range_concurrency\" type:\"number\" default:\"4\" help:\"Chunk mode concurrent upstream requests\"`\n\tRangePercent       int    `json:\"range_percent\" type:\"number\" default:\"15\" help:\"Percent mode part size percentage (1-100)\"`\n\tEnableRangeControl bool   `json:\"enable_range_control\" default:\"true\" help:\"Enable driver-level range shaping for smoother streaming\"`\n}\n\nvar config = driver.Config{\n\tName:              \"Streamtape\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         true,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\n\tProxyRangeOption:  true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Streamtape{}\n\t})\n}\n"
  },
  {
    "path": "drivers/streamtape/types.go",
    "content": "package streamtape\n\nimport \"encoding/json\"\n\ntype apiResponse struct {\n\tStatus int             `json:\"status\"`\n\tMsg    string          `json:\"msg\"`\n\tResult json.RawMessage `json:\"result\"`\n}\n\ntype accountInfo struct {\n\tAPIID    string `json:\"apiid\"`\n\tEmail    string `json:\"email\"`\n\tSignupAt string `json:\"signup_at\"`\n}\n\ntype listFolderResult struct {\n\tFolders []folderItem `json:\"folders\"`\n\tFiles   []fileItem   `json:\"files\"`\n}\n\ntype folderItem struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype fileItem struct {\n\tName      string `json:\"name\"`\n\tSize      int64  `json:\"size\"`\n\tLink      string `json:\"link\"`\n\tCreatedAt int64  `json:\"created_at\"`\n\tDownloads int64  `json:\"downloads\"`\n\tLinkID    string `json:\"linkid\"`\n\tConvert   string `json:\"convert\"`\n}\n\ntype dlTicketResult struct {\n\tTicket   string `json:\"ticket\"`\n\tWaitTime int    `json:\"wait_time\"`\n}\n\ntype dlResult struct {\n\tName string `json:\"name\"`\n\tSize int64  `json:\"size\"`\n\tURL  string `json:\"url\"`\n}\n\ntype createFolderResult struct {\n\tFolderID string `json:\"folderid\"`\n}\n\ntype uploadURLResult struct {\n\tURL string `json:\"url\"`\n}\n"
  },
  {
    "path": "drivers/streamtape/util.go",
    "content": "package streamtape\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\nconst apiBase = \"https://api.streamtape.com\"\n\nfunc (d *Streamtape) callAPI(ctx context.Context, endpoint string, params map[string]string, out any) error {\n\tquery := map[string]string{\n\t\t\"login\": d.APILogin,\n\t\t\"key\":   d.APIKey,\n\t}\n\tfor k, v := range params {\n\t\tif strings.TrimSpace(v) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tquery[k] = v\n\t}\n\n\tvar resp apiResponse\n\tr, err := base.RestyClient.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(query).\n\t\tSetResult(&resp).\n\t\tGet(apiBase + endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif r.StatusCode() != http.StatusOK {\n\t\treturn fmt.Errorf(\"streamtape http error: %d\", r.StatusCode())\n\t}\n\tif resp.Status != 200 {\n\t\treturn fmt.Errorf(\"streamtape api error: status=%d msg=%s\", resp.Status, resp.Msg)\n\t}\n\tif out == nil || len(resp.Result) == 0 || string(resp.Result) == \"null\" {\n\t\treturn nil\n\t}\n\tif err := json.Unmarshal(resp.Result, out); err != nil {\n\t\treturn fmt.Errorf(\"decode streamtape result failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc folderIDFromObjID(id string) string {\n\tif id == \"\" || id == \"0\" || id == \"/\" {\n\t\treturn \"0\"\n\t}\n\tif strings.HasPrefix(id, \"d:\") {\n\t\treturn strings.TrimPrefix(id, \"d:\")\n\t}\n\treturn id\n}\n\nfunc fileIDFromObjID(id string) string {\n\tif strings.HasPrefix(id, \"f:\") {\n\t\treturn strings.TrimPrefix(id, \"f:\")\n\t}\n\treturn id\n}\n\nfunc encodeFolderID(id string) string {\n\tif id == \"\" || id == \"0\" || id == \"/\" {\n\t\treturn \"d:0\"\n\t}\n\treturn \"d:\" + id\n}\n\nfunc encodeFileID(id string) string {\n\tif strings.HasPrefix(id, \"f:\") {\n\t\treturn id\n\t}\n\treturn \"f:\" + id\n}\n\nfunc extractFileIDFromLink(link string) string {\n\tif link == \"\" {\n\t\treturn \"\"\n\t}\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tparts := strings.Split(strings.Trim(path.Clean(u.Path), \"/\"), \"/\")\n\tfor i := 0; i < len(parts)-1; i++ {\n\t\tif parts[i] == \"v\" {\n\t\t\treturn parts[i+1]\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc buildFileObj(f fileItem) model.Obj {\n\tid := f.LinkID\n\tif id == \"\" {\n\t\tid = extractFileIDFromLink(f.Link)\n\t}\n\tmod := time.Now()\n\tif f.CreatedAt > 0 {\n\t\tmod = time.Unix(f.CreatedAt, 0)\n\t}\n\treturn &model.Object{\n\t\tID:       encodeFileID(id),\n\t\tName:     f.Name,\n\t\tSize:     f.Size,\n\t\tModified: mod,\n\t\tIsFolder: false,\n\t}\n}\n\nfunc extractFileIDFromUploadBody(body []byte) string {\n\tif len(body) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar resp apiResponse\n\tif err := json.Unmarshal(body, &resp); err != nil {\n\t\treturn \"\"\n\t}\n\tif resp.Status != 200 || len(resp.Result) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar result map[string]any\n\tif err := json.Unmarshal(resp.Result, &result); err != nil {\n\t\treturn \"\"\n\t}\n\tfor _, key := range []string{\"file\", \"fileid\", \"id\", \"linkid\"} {\n\t\tif v, ok := result[key]; ok {\n\t\t\tif s, ok := v.(string); ok && s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "drivers/strm/driver.go",
    "content": "package strm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Strm struct {\n\tmodel.Storage\n\tAddition\n\n\taliases       map[string][]string\n\tautoFlatten   bool\n\tsingleRootKey string\n\n\tmediaExtSet      map[string]struct{}\n\tdownloadExtSet   map[string]struct{}\n\tnormalizedMode   string\n\tnormalizedPrefix string\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 strings.TrimSpace(d.Paths) == \"\" {\n\t\treturn errors.New(\"paths is required\")\n\t}\n\tif d.SaveStrmToLocal && strings.TrimSpace(d.SaveStrmLocalPath) == \"\" {\n\t\treturn errors.New(\"SaveStrmLocalPath is required\")\n\t}\n\n\td.aliases = parseAliases(d.Paths)\n\tif len(d.aliases) == 0 {\n\t\treturn errors.New(\"no valid path mapping found\")\n\t}\n\n\td.autoFlatten = len(d.aliases) == 1\n\td.singleRootKey = \"\"\n\tif d.autoFlatten {\n\t\tfor k := range d.aliases {\n\t\t\td.singleRootKey = k\n\t\t}\n\t}\n\n\td.mediaExtSet = parseExtSet(defaultIfEmpty(d.FilterFileTypes, defaultMediaExt))\n\td.downloadExtSet = parseExtSet(defaultIfEmpty(d.DownloadFileTypes, defaultDownloadExt))\n\td.normalizedPrefix = normalizePrefix(defaultIfEmpty(d.PathPrefix, \"/d\"))\n\td.normalizedMode = normalizeSaveMode(d.SaveLocalMode)\n\n\tif d.Version != 5 {\n\t\td.FilterFileTypes = mergeDefaultExtCSV(d.FilterFileTypes, defaultMediaExt)\n\t\td.DownloadFileTypes = mergeDefaultExtCSV(d.DownloadFileTypes, defaultDownloadExt)\n\t\td.PathPrefix = \"/d\"\n\t\td.Version = 5\n\t}\n\tif d.SaveLocalMode == \"\" {\n\t\td.SaveLocalMode = SaveLocalInsertMode\n\t}\n\tif d.SignExpireHours < 0 {\n\t\td.SignExpireHours = 0\n\t}\n\tif d.RotateSignNow {\n\t\td.RotateSignNow = false\n\t\top.MustSaveDriverStorage(d)\n\t\tif d.SaveStrmToLocal && strings.TrimSpace(d.SaveStrmLocalPath) != \"\" {\n\t\t\tgo func() {\n\t\t\t\tlog.Infof(\"strm: start rotating signs for [%s]\", d.MountPath)\n\t\t\t\td.rotateAllLocal(context.Background())\n\t\t\t\tlog.Infof(\"strm: finished rotating signs for [%s]\", d.MountPath)\n\t\t\t}()\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Strm) Drop(ctx context.Context) error {\n\td.aliases = nil\n\td.mediaExtSet = nil\n\td.downloadExtSet = nil\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\tpath = cleanPath(path)\n\troot, sub := d.splitVirtualPath(path)\n\ttargets, ok := d.aliases[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tfor _, targetRoot := range targets {\n\t\trealPath := stdpath.Join(targetRoot, sub)\n\t\tobj, err := fs.Get(ctx, realPath, &fs.GetArgs{NoLog: true})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif obj.IsDir() {\n\t\t\treturn wrapObj(path, obj, 0), nil\n\t\t}\n\t\treturn wrapObj(realPath, obj, obj.GetSize()), nil\n\t}\n\n\tif strings.HasSuffix(strings.ToLower(path), \".strm\") {\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\tvirtualDir := cleanPath(dir.GetPath())\n\tif virtualDir == \"/\" && !d.autoFlatten {\n\t\tobjs := d.listVirtualRoots()\n\t\td.syncLocalDir(ctx, virtualDir, objs)\n\t\treturn objs, nil\n\t}\n\n\troot, sub := d.splitVirtualPath(virtualDir)\n\ttargets, ok := d.aliases[root]\n\tif !ok {\n\t\treturn nil, errs.ObjectNotFound\n\t}\n\n\tout := make([]model.Obj, 0)\n\tfor _, targetRoot := range targets {\n\t\trealDir := stdpath.Join(targetRoot, sub)\n\t\tobjs, err := fs.List(ctx, realDir, &fs.ListArgs{NoLog: true, Refresh: args.Refresh})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, d.mapListedObjects(ctx, realDir, objs)...)\n\t}\n\n\td.syncLocalDir(ctx, virtualDir, out)\n\treturn out, 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\tline := d.buildStrmLine(ctx, file.GetPath())\n\t\treturn &model.Link{MFile: model.NewNopMFile(strings.NewReader(line + \"\\n\"))}, nil\n\t}\n\treturn d.linkRealFile(ctx, file.GetPath(), args)\n}\n\nfunc (d *Strm) listVirtualRoots() []model.Obj {\n\tobjs := make([]model.Obj, 0, len(d.aliases))\n\tfor k := range d.aliases {\n\t\tobjs = append(objs, &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}\n\treturn objs\n}\n\nfunc (d *Strm) rotateAllLocal(ctx context.Context) {\n\tfor alias, roots := range d.aliases {\n\t\tvirtualRoot := \"/\"\n\t\tif !d.autoFlatten {\n\t\t\tvirtualRoot = \"/\" + alias\n\t\t}\n\t\tfor _, realRoot := range roots {\n\t\t\td.walkAndSync(ctx, virtualRoot, realRoot)\n\t\t}\n\t}\n}\n\nfunc (d *Strm) walkAndSync(ctx context.Context, virtualDir, realDir string) {\n\tobjs, err := fs.List(ctx, realDir, &fs.ListArgs{NoLog: true, Refresh: true})\n\tif err != nil {\n\t\tlog.Warnf(\"strm: rotate list failed %s: %v\", realDir, err)\n\t\treturn\n\t}\n\tmapped := d.mapListedObjects(ctx, realDir, objs)\n\td.syncLocalDirWithMode(ctx, virtualDir, mapped, SaveLocalUpdateMode)\n\tfor _, obj := range objs {\n\t\tif !obj.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tchildVirtual := stdpath.Join(virtualDir, obj.GetName())\n\t\tchildReal := stdpath.Join(realDir, obj.GetName())\n\t\td.walkAndSync(ctx, childVirtual, childReal)\n\t}\n}\n\nfunc (d *Strm) mapListedObjects(ctx context.Context, realDir string, listed []model.Obj) []model.Obj {\n\tret := make([]model.Obj, 0, len(listed))\n\tfor _, obj := range listed {\n\t\tif obj.IsDir() {\n\t\t\tret = append(ret, &model.Object{\n\t\t\t\tName:     obj.GetName(),\n\t\t\t\tPath:     \"\",\n\t\t\t\tIsFolder: true,\n\t\t\t\tModified: obj.ModTime(),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\trealPath := stdpath.Join(realDir, obj.GetName())\n\t\text := fileExt(obj.GetName())\n\n\t\tif _, ok := d.downloadExtSet[ext]; ok {\n\t\t\tret = append(ret, d.cloneWithPath(obj, realPath, obj.GetName(), \"\", obj.GetSize()))\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := d.mediaExtSet[ext]; ok {\n\t\t\tstrmName := strings.TrimSuffix(obj.GetName(), stdpath.Ext(obj.GetName())) + \".strm\"\n\t\t\tsize := int64(len(d.buildStrmLine(ctx, realPath)) + 1)\n\t\t\tret = append(ret, d.cloneWithPath(obj, realPath, strmName, \"strm\", size))\n\t\t}\n\t}\n\treturn ret\n}\n\nfunc (d *Strm) cloneWithPath(src model.Obj, realPath, name, id string, size int64) model.Obj {\n\tbaseObj := model.Object{\n\t\tID:       id,\n\t\tPath:     realPath,\n\t\tName:     name,\n\t\tSize:     size,\n\t\tModified: src.ModTime(),\n\t\tIsFolder: src.IsDir(),\n\t}\n\tthumb, ok := model.GetThumb(src)\n\tif !ok {\n\t\treturn &baseObj\n\t}\n\treturn &model.ObjThumb{Object: baseObj, Thumbnail: model.Thumbnail{Thumbnail: thumb}}\n}\n\nfunc (d *Strm) splitVirtualPath(path string) (string, string) {\n\tif d.autoFlatten {\n\t\treturn d.singleRootKey, path\n\t}\n\ttrimmed := strings.TrimPrefix(path, \"/\")\n\tparts := strings.SplitN(trimmed, \"/\", 2)\n\tif len(parts) == 1 {\n\t\treturn parts[0], \"\"\n\t}\n\treturn parts[0], parts[1]\n}\n\nfunc cleanPath(path string) string {\n\tif path == \"\" {\n\t\treturn \"/\"\n\t}\n\treturn filepath.ToSlash(stdpath.Clean(\"/\" + strings.TrimPrefix(path, \"/\")))\n}\n\nfunc wrapObj(path string, src model.Obj, size int64) model.Obj {\n\treturn &model.Object{\n\t\tPath:     path,\n\t\tName:     src.GetName(),\n\t\tSize:     size,\n\t\tModified: src.ModTime(),\n\t\tIsFolder: src.IsDir(),\n\t\tHashInfo: src.GetHash(),\n\t}\n}\n\nvar _ driver.Driver = (*Strm)(nil)\n"
  },
  {
    "path": "drivers/strm/hook.go",
    "content": "package strm\n\n// Local sync is triggered during STRM directory listing.\n"
  },
  {
    "path": "drivers/strm/meta.go",
    "content": "package strm\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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 generated strm file\"`\n\tPathPrefix        string `json:\"PathPrefix\" type:\"text\" required:\"false\" default:\"/d\" help:\"Path prefix in strm content\"`\n\tDownloadFileTypes string `json:\"downloadFileTypes\" type:\"text\" default:\"ass,srt,vtt,sub,strm\" required:\"false\" help:\"Extensions to download as local files\"`\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:\"Extensions to expose as .strm\"`\n\tEncodePath        bool   `json:\"encodePath\" default:\"true\" required:\"true\" help:\"Encode path in strm content\"`\n\tWithoutUrl        bool   `json:\"withoutUrl\" default:\"false\" help:\"Generate path-only strm content\"`\n\tWithSign          bool   `json:\"withSign\" default:\"false\" help:\"Append sign query to generated URL\"`\n\tSignExpireHours   int    `json:\"SignExpireHours\" type:\"number\" default:\"0\" help:\"Driver-level sign expiration in hours. 0 uses global link_expiration\"`\n\tRotateSignNow     bool   `json:\"RotateSignNow\" type:\"bool\" default:\"false\" help:\"Set true and save to rotate signs now (rewrite local STRM), then auto reset to false\"`\n\tSaveStrmToLocal   bool   `json:\"SaveStrmToLocal\" default:\"false\" help:\"Save generated files to local disk\"`\n\tSaveStrmLocalPath string `json:\"SaveStrmLocalPath\" type:\"text\" help:\"Local path for generated files\"`\n\tSaveLocalMode     string `json:\"SaveLocalMode\" type:\"select\" help:\"Local save 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}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Strm{Addition: Addition{EncodePath: true}}\n\t})\n}\n"
  },
  {
    "path": "drivers/strm/util.go",
    "content": "package strm\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\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tpkgerr \"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tdefaultMediaExt    = \"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac\"\n\tdefaultDownloadExt = \"ass,srt,vtt,sub,strm\"\n)\n\nfunc parseAliases(raw string) map[string][]string {\n\taliases := map[string][]string{}\n\tfor _, line := range strings.Split(raw, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tname, target := parseAliasLine(line)\n\t\taliases[name] = append(aliases[name], cleanPath(target))\n\t}\n\treturn aliases\n}\n\nfunc parseAliasLine(line string) (string, string) {\n\tif strings.Contains(line, \":\") {\n\t\tparts := strings.SplitN(line, \":\", 2)\n\t\tif !strings.Contains(parts[0], \"/\") {\n\t\t\treturn parts[0], parts[1]\n\t\t}\n\t}\n\treturn stdpath.Base(line), line\n}\n\nfunc parseExtSet(csv string) map[string]struct{} {\n\tret := map[string]struct{}{}\n\tfor _, part := range strings.Split(csv, \",\") {\n\t\text := normalizeExt(part)\n\t\tif ext != \"\" {\n\t\t\tret[ext] = struct{}{}\n\t\t}\n\t}\n\treturn ret\n}\n\nfunc mergeDefaultExtCSV(csv, defaults string) string {\n\tbase := parseExtSet(csv)\n\tfor ext := range parseExtSet(defaults) {\n\t\tbase[ext] = struct{}{}\n\t}\n\tkeys := make([]string, 0, len(base))\n\tfor k := range base {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\treturn strings.Join(keys, \",\")\n}\n\nfunc normalizeExt(ext string) string {\n\text = strings.ToLower(strings.TrimSpace(ext))\n\text = strings.TrimPrefix(ext, \".\")\n\treturn ext\n}\n\nfunc fileExt(name string) string {\n\treturn normalizeExt(stdpath.Ext(name))\n}\n\nfunc defaultIfEmpty(v, fallback string) string {\n\tif strings.TrimSpace(v) == \"\" {\n\t\treturn fallback\n\t}\n\treturn v\n}\n\nfunc normalizeSaveMode(mode string) string {\n\tswitch strings.ToLower(strings.TrimSpace(mode)) {\n\tcase \"sync\":\n\t\treturn SaveLocalSyncMode\n\tcase \"update\":\n\t\treturn SaveLocalUpdateMode\n\tcase \"insert\", \"missing\":\n\t\treturn SaveLocalInsertMode\n\tdefault:\n\t\treturn SaveLocalInsertMode\n\t}\n}\n\nfunc normalizePrefix(prefix string) string {\n\tprefix = strings.TrimSpace(prefix)\n\tif prefix == \"\" {\n\t\treturn \"/d\"\n\t}\n\tif !strings.HasPrefix(prefix, \"/\") {\n\t\tprefix = \"/\" + prefix\n\t}\n\treturn prefix\n}\n\nfunc (d *Strm) buildStrmLine(ctx context.Context, realPath string) string {\n\tpathPart := realPath\n\tif d.EncodePath {\n\t\tpathPart = utils.EncodePath(pathPart, true)\n\t}\n\tif d.WithSign {\n\t\tsep := \"?\"\n\t\tif strings.Contains(pathPart, \"?\") {\n\t\t\tsep = \"&\"\n\t\t}\n\t\tpathPart += sep + \"sign=\" + d.generateSign(realPath)\n\t}\n\tjoined := stdpath.Join(d.normalizedPrefix, pathPart)\n\tif !strings.HasPrefix(joined, \"/\") {\n\t\tjoined = \"/\" + joined\n\t}\n\tif d.WithoutUrl {\n\t\treturn joined\n\t}\n\tbaseURL := strings.TrimSpace(d.SiteUrl)\n\tif baseURL == \"\" {\n\t\tif c, ok := ctx.(*gin.Context); ok {\n\t\t\tbaseURL = common.GetApiUrl(c.Request)\n\t\t} else {\n\t\t\tbaseURL = common.GetApiUrl(nil)\n\t\t}\n\t}\n\tbaseURL = strings.TrimSuffix(baseURL, \"/\")\n\treturn baseURL + joined\n}\n\nfunc (d *Strm) linkRealFile(ctx context.Context, realPath string, args model.LinkArgs) (*model.Link, error) {\n\tstorage, actualPath, err := op.GetStorageAndActualPath(realPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !args.Redirect {\n\t\tlink, _, linkErr := op.Link(ctx, storage, actualPath, args)\n\t\treturn link, linkErr\n\t}\n\tobj, err := fs.Get(ctx, realPath, &fs.GetArgs{NoLog: true})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif common.ShouldProxy(storage, obj.GetName()) {\n\t\tapi := common.GetApiUrl(args.HttpReq)\n\t\tif api == \"\" {\n\t\t\tapi = strings.TrimSuffix(strings.TrimSpace(d.SiteUrl), \"/\")\n\t\t}\n\t\tif api == \"\" {\n\t\t\tapi = common.GetApiUrl(nil)\n\t\t}\n\t\treturn &model.Link{URL: fmt.Sprintf(\"%s/p%s?sign=%s\", api, utils.EncodePath(realPath, true), d.generateSign(realPath))}, nil\n\t}\n\tlink, _, linkErr := op.Link(ctx, storage, actualPath, args)\n\treturn link, linkErr\n}\n\nfunc (d *Strm) syncLocalDir(ctx context.Context, virtualDir string, objs []model.Obj) {\n\td.syncLocalDirWithMode(ctx, virtualDir, objs, d.normalizedMode)\n}\n\nfunc (d *Strm) syncLocalDirWithMode(ctx context.Context, virtualDir string, objs []model.Obj, mode string) {\n\tif !d.SaveStrmToLocal || strings.TrimSpace(d.SaveStrmLocalPath) == \"\" {\n\t\treturn\n\t}\n\tbaseDir := filepath.Clean(d.SaveStrmLocalPath)\n\tlocalDir := baseDir\n\tif virtualDir != \"/\" {\n\t\tlocalDir = filepath.Join(baseDir, filepath.FromSlash(strings.TrimPrefix(virtualDir, \"/\")))\n\t}\n\tif err := os.MkdirAll(localDir, 0o755); err != nil {\n\t\tlog.Warnf(\"strm: mkdir failed %s: %v\", localDir, err)\n\t\treturn\n\t}\n\n\texpected := map[string]bool{}\n\tfor _, obj := range objs {\n\t\tname := obj.GetName()\n\t\texpected[name] = obj.IsDir()\n\t\tlocalPath := filepath.Join(localDir, name)\n\t\tif obj.IsDir() {\n\t\t\t_ = os.MkdirAll(localPath, 0o755)\n\t\t\tcontinue\n\t\t}\n\t\tpayload, err := d.localPayload(ctx, obj)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"strm: build local payload failed %s: %v\", localPath, err)\n\t\t\tcontinue\n\t\t}\n\t\tif err = d.writeLocal(localPath, payload, mode); err != nil {\n\t\t\tlog.Warnf(\"strm: write local failed %s: %v\", localPath, err)\n\t\t}\n\t}\n\n\tif mode == SaveLocalSyncMode {\n\t\td.syncDeleteExtras(localDir, expected)\n\t}\n}\n\nfunc (d *Strm) localPayload(ctx context.Context, obj model.Obj) ([]byte, error) {\n\tif obj.GetID() == \"strm\" {\n\t\treturn []byte(d.buildStrmLine(ctx, obj.GetPath()) + \"\\n\"), nil\n\t}\n\tlink, err := d.linkRealFile(ctx, obj.GetPath(), model.LinkArgs{Redirect: true})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn readLinkBytes(ctx, link)\n}\n\nfunc readLinkBytes(ctx context.Context, link *model.Link) ([]byte, error) {\n\tif link.MFile != nil {\n\t\tdefer link.MFile.Close()\n\t\treturn io.ReadAll(link.MFile)\n\t}\n\tif link.RangeReadCloser != nil {\n\t\trc, err := link.RangeReadCloser.RangeRead(ctx, http_range.Range{Length: -1})\n\t\tif err == nil && rc != nil {\n\t\t\tdefer rc.Close()\n\t\t\treturn io.ReadAll(rc)\n\t\t}\n\t}\n\tif link.URL == \"\" {\n\t\treturn nil, fmt.Errorf(\"empty link\")\n\t}\n\turl := link.URL\n\tif !strings.HasPrefix(url, \"http://\") && !strings.HasPrefix(url, \"https://\") {\n\t\tapi := common.GetApiUrl(nil)\n\t\tif api == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"relative url without site url: %s\", url)\n\t\t}\n\t\turl = strings.TrimSuffix(api, \"/\") + url\n\t}\n\tres, err := base.RestyClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.RawBody().Close()\n\tif res.StatusCode() >= http.StatusBadRequest {\n\t\treturn nil, fmt.Errorf(\"read url failed: status=%d\", res.StatusCode())\n\t}\n\treturn io.ReadAll(res.RawBody())\n}\n\nfunc (d *Strm) writeLocal(path string, payload []byte, mode string) error {\n\tif mode == SaveLocalInsertMode && utils.Exists(path) {\n\t\treturn nil\n\t}\n\tif st, err := os.Stat(path); err == nil && st.IsDir() {\n\t\tif mode != SaveLocalSyncMode {\n\t\t\treturn nil\n\t\t}\n\t\tif err = os.RemoveAll(path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif mode != SaveLocalInsertMode {\n\t\tif old, err := os.ReadFile(path); err == nil {\n\t\t\tif bytes.Equal(old, payload) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tf, err := utils.CreateNestedFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\t_, err = f.Write(payload)\n\treturn err\n}\n\nfunc (d *Strm) syncDeleteExtras(localDir string, expected map[string]bool) {\n\tentries, err := os.ReadDir(localDir)\n\tif err != nil {\n\t\tif pkgerr.Cause(err) != os.ErrNotExist {\n\t\t\tlog.Warnf(\"strm: read local dir failed %s: %v\", localDir, err)\n\t\t}\n\t\treturn\n\t}\n\tfor _, e := range entries {\n\t\texpectDir, ok := expected[e.Name()]\n\t\tfull := filepath.Join(localDir, e.Name())\n\t\tif !ok || expectDir != e.IsDir() {\n\t\t\t_ = os.RemoveAll(full)\n\t\t}\n\t}\n}\n\nfunc (d *Strm) generateSign(path string) string {\n\tif d.SignExpireHours > 0 {\n\t\treturn sign.WithDuration(path, time.Duration(d.SignExpireHours)*time.Hour)\n\t}\n\treturn sign.Sign(path)\n}\n"
  },
  {
    "path": "drivers/teambition/driver.go",
    "content": "package teambition\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"net/http\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/template/driver.go",
    "content": "package template\n\nimport (\n\t\"context\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal:         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}\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\t\"math\"\n\tstdpath \"path\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\treturn fileToObj(src), 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.CacheFullInTempFile()\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\t\"app_id\":     \"250528\",\n\t\t\"web\":        \"1\",\n\t\t\"channel\":    \"dubox\",\n\t\t\"clienttype\": \"0\",\n\t}\n\n\tstreamSize := stream.GetSize()\n\tchunkSize := calculateChunkSize(streamSize)\n\tchunkByteData := make([]byte, chunkSize)\n\tcount := int(math.Ceil(float64(streamSize) / float64(chunkSize)))\n\tleft := streamSize\n\tuploadBlockList := make([]string, 0, count)\n\th := md5.New()\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\tuploadBlockList = append(uploadBlockList, hex.EncodeToString(h.Sum(nil)))\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\tres, err := base.RestyClient.R().\n\t\t\tSetContext(ctx).\n\t\t\tSetQueryParams(params).\n\t\t\tSetFileReader(\"file\", stream.GetName(), driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData))).\n\t\t\tSetHeader(\"Cookie\", d.Cookie).\n\t\t\tPost(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debugln(res.String())\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\":       base.UserAgent,\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\":       base.UserAgent,\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\tres, err := req.Execute(method, d.base_url+rurl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terrno := utils.Json.Get(res.Body(), \"errno\").ToInt()\n\tif errno == 4000023 {\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) 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\", base.UserAgent).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{base.UserAgent},\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{base.UserAgent},\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\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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.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:          \"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\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\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\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 *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})\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})\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{\"name\": newName})\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})\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.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.CacheFullInTempFileAndHash(file, 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 *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\":      \"\",\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// 设置刷新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\"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})\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})\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\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\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\tOnlyProxy: 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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\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\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\tstreamPkg \"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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\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}\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\tif x.DeviceID == \"\" {\n\t\tx.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))\n\t}\n\tx.XunLeiBrowserCommon.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}\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\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.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.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.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\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}\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.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\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.CacheFullInTempFileAndHash(stream, 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) 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\tfolderSpace = ThunderBrowserDriveSpace\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// 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.UserID); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\tdefault:\n\t\treturn nil, err\n\t}\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\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\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"
  },
  {
    "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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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:\"uWRwO7gPfdPB/0NfPtfQO+71,F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V,0HbpxvpXFsBK5CoTKam,dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv,SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI,unqfo7Z64Rie9RNHMOB,7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf,RBG,ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A\"`\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:\"ZUBzD9J_XPXfn7f7\"`\n\tClientSecret  string `json:\"client_secret\"  required:\"true\" default:\"yESVmHecEe6F0aou69vl-g\"`\n\tClientVersion string `json:\"client_version\"  required:\"true\" default:\"1.10.0.2633\"`\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// 移除方式\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\tUseVideoUrl  bool   `json:\"use_video_url\" default:\"false\"`\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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\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\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\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"
  },
  {
    "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\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\tXLUSER_API_URL = \"https://xluser-ssl.xunlei.com/v1\"\n)\n\nvar Algorithms = []string{\n\t\"uWRwO7gPfdPB/0NfPtfQO+71\",\n\t\"F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V\",\n\t\"0HbpxvpXFsBK5CoTKam\",\n\t\"dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv\",\n\t\"SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI\",\n\t\"unqfo7Z64Rie9RNHMOB\",\n\t\"7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf\",\n\t\"RBG\",\n\t\"ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A\",\n}\n\nconst (\n\tClientID          = \"ZUBzD9J_XPXfn7f7\"\n\tClientSecret      = \"yESVmHecEe6F0aou69vl-g\"\n\tClientVersion     = \"1.10.0.2633\"\n\tPackageName       = \"com.xunlei.browser\"\n\tDownloadUserAgent = \"AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)\"\n\tSdkVersion        = \"233100\"\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)\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\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\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\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\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\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, \"22062\", \"a5d7416858147a4ab99573872ffccef8\")\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 \", \"22062\"))\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\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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.CacheFullInTempFileAndHash(file, 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) 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\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.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"
  },
  {
    "path": "drivers/thunderx/meta.go",
    "content": "package thunderx\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyProxy: false,\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\thash_extend \"github.com/alist-org/alist/v3/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"
  },
  {
    "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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\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/trainbit/driver.go",
    "content": "package trainbit\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\ntype Trainbit struct {\n\tmodel.Storage\n\tAddition\n}\n\nvar apiExpiredate, guid string\n\nfunc (d *Trainbit) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Trainbit) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Trainbit) Init(ctx context.Context) error {\n\tbase.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\treturn http.ErrUseLastResponse\n\t}\n\tvar err error\n\tapiExpiredate, guid, err = getToken(d.ApiKey, d.AUSHELLPORTAL)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *Trainbit) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Trainbit) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tform := make(url.Values)\n\tform.Set(\"parentid\", strings.Split(dir.GetID(), \"_\")[0])\n\tres, err := postForm(\"https://trainbit.com/lib/api/v1/listoffiles\", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar jsonData any\n\terr = json.Unmarshal(data, &jsonData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobject, err := parseRawFileObject(jsonData.(map[string]any)[\"items\"].([]any))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn object, nil\n}\n\nfunc (d *Trainbit) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tres, err := get(fmt.Sprintf(\"https://trainbit.com/files/%s/\", strings.Split(file.GetID(), \"_\")[0]), d.ApiKey, d.AUSHELLPORTAL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &model.Link{\n\t\tURL: res.Header.Get(\"Location\"),\n\t}, nil\n}\n\nfunc (d *Trainbit) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tform := make(url.Values)\n\tform.Set(\"name\", local2provider(dirName, true))\n\tform.Set(\"parentid\", strings.Split(parentDir.GetID(), \"_\")[0])\n\t_, err := postForm(\"https://trainbit.com/lib/api/v1/createfolder\", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)\n\treturn err\n}\n\nfunc (d *Trainbit) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tform := make(url.Values)\n\tform.Set(\"sourceid\", strings.Split(srcObj.GetID(), \"_\")[0])\n\tform.Set(\"destinationid\", strings.Split(dstDir.GetID(), \"_\")[0])\n\t_, err := postForm(\"https://trainbit.com/lib/api/v1/move\", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)\n\treturn err\n}\n\nfunc (d *Trainbit) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tform := make(url.Values)\n\tform.Set(\"id\", strings.Split(srcObj.GetID(), \"_\")[0])\n\tform.Set(\"name\", local2provider(newName, srcObj.IsDir()))\n\t_, err := postForm(\"https://trainbit.com/lib/api/v1/edit\", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)\n\treturn err\n}\n\nfunc (d *Trainbit) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\treturn errs.NotImplement\n}\n\nfunc (d *Trainbit) Remove(ctx context.Context, obj model.Obj) error {\n\tform := make(url.Values)\n\tform.Set(\"id\", strings.Split(obj.GetID(), \"_\")[0])\n\t_, err := postForm(\"https://trainbit.com/lib/api/v1/delete\", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)\n\treturn err\n}\n\nfunc (d *Trainbit) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {\n\tendpoint, _ := url.Parse(\"https://tb28.trainbit.com/api/upload/send_raw/\")\n\tquery := &url.Values{}\n\tquery.Add(\"q\", strings.Split(dstDir.GetID(), \"_\")[1])\n\tquery.Add(\"guid\", guid)\n\tquery.Add(\"name\", url.QueryEscape(local2provider(s.GetName(), false)+\".\"))\n\tendpoint.RawQuery = query.Encode()\n\tprogressReader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{\n\t\tReader:         s,\n\t\tUpdateProgress: up,\n\t})\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), progressReader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", \"text/json; charset=UTF-8\")\n\t_, err = base.HttpClient.Do(req)\n\treturn err\n}\n\nvar _ driver.Driver = (*Trainbit)(nil)\n"
  },
  {
    "path": "drivers/trainbit/meta.go",
    "content": "package trainbit\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tAUSHELLPORTAL string `json:\"AUSHELLPORTAL\" required:\"true\"`\n\tApiKey string `json:\"apikey\" required:\"true\"`\n}\n\nvar config = driver.Config{\n\tName:          \"Trainbit\",\n\tLocalSort:     false,\n\tOnlyLocal:     false,\n\tOnlyProxy:     false,\n\tNoCache:       false,\n\tNoUpload:      false,\n\tNeedMs:        false,\n\tDefaultRoot:   \"0_000\",\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Trainbit{}\n\t})\n}\n"
  },
  {
    "path": "drivers/trainbit/types.go",
    "content": "package trainbit"
  },
  {
    "path": "drivers/trainbit/util.go",
    "content": "package trainbit\n\nimport (\n\t\"html\"\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\nfunc get(url string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.AddCookie(&http.Cookie{\n\t\tName:   \".AUSHELLPORTAL\",\n\t\tValue:  AUSHELLPORTAL,\n\t\tMaxAge: 2 * 60,\n\t})\n\treq.AddCookie(&http.Cookie{\n\t\tName:   \"retkeyapi\",\n\t\tValue:  apiKey,\n\t\tMaxAge: 2 * 60,\n\t})\n\tres, err := base.HttpClient.Do(req)\n\treturn res, err\n}\n\nfunc postForm(endpoint string, data url.Values, apiExpiredate string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {\n\textData := make(url.Values)\n\tfor key, value := range data {\n\t\textData[key] = make([]string, len(value))\n\t\tcopy(extData[key], value)\n\t}\n\textData.Set(\"apikey\", apiKey)\n\textData.Set(\"expiredate\", apiExpiredate)\n\treq, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(extData.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.AddCookie(&http.Cookie{\n\t\tName:   \".AUSHELLPORTAL\",\n\t\tValue:  AUSHELLPORTAL,\n\t\tMaxAge: 2 * 60,\n\t})\n\treq.AddCookie(&http.Cookie{\n\t\tName:   \"retkeyapi\",\n\t\tValue:  apiKey,\n\t\tMaxAge: 2 * 60,\n\t})\n\tres, err := base.HttpClient.Do(req)\n\treturn res, err\n}\n\nfunc getToken(apiKey string, AUSHELLPORTAL string) (string, string, error) {\n\tres, err := get(\"https://trainbit.com/files/\", apiKey, AUSHELLPORTAL)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\ttext := string(data)\n\tapiExpiredateReg := regexp.MustCompile(`core.api.expiredate = '([^']*)';`)\n\tresult := apiExpiredateReg.FindAllStringSubmatch(text, -1)\n\tapiExpiredate := result[0][1]\n\tguidReg := regexp.MustCompile(`app.vars.upload.guid = '([^']*)';`)\n\tresult = guidReg.FindAllStringSubmatch(text, -1)\n\tguid := result[0][1]\n\treturn apiExpiredate, guid, nil\n}\n\nfunc local2provider(filename string, isFolder bool) string {\n\tif isFolder {\n\t\treturn filename\n\t}\n\treturn filename + \".delete_suffix\"\n}\n\nfunc provider2local(filename string) string {\n\tfilename = html.UnescapeString(filename)\n\tindex := strings.LastIndex(filename, \".delete_suffix\")\n\tif index != -1 {\n\t\tfilename = filename[:index]\n\t}\n\treturn filename\n}\n\nfunc parseRawFileObject(rawObject []any) ([]model.Obj, error) {\n\tobjectList := make([]model.Obj, 0)\n\tfor _, each := range rawObject {\n\t\tobject := each.(map[string]any)\n\t\tif object[\"id\"].(string) == \"0\" {\n\t\t\tcontinue\n\t\t}\n\t\tisFolder := int64(object[\"ty\"].(float64)) == 1\n\t\tvar name string\n\t\tif object[\"ext\"].(string) != \"\" {\n\t\t\tname = strings.Join([]string{object[\"name\"].(string), object[\"ext\"].(string)}, \".\")\n\t\t} else {\n\t\t\tname = object[\"name\"].(string)\n\t\t}\n\t\tmodified, err := time.Parse(\"2006/01/02 15:04:05\", object[\"modified\"].(string))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tobjectList = append(objectList, model.Obj(&model.Object{\n\t\t\tID:       strings.Join([]string{object[\"id\"].(string), strings.Split(object[\"uploadurl\"].(string), \"=\")[1]}, \"_\"),\n\t\t\tName:     provider2local(name),\n\t\t\tSize:     int64(object[\"byte\"].(float64)),\n\t\t\tModified: modified.Add(-210 * time.Minute),\n\t\t\tIsFolder: isFolder,\n\t\t}))\n\t}\n\treturn objectList, nil\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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 (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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\t// Usually one of two\n\t// driver.RootPath\n\t// driver.RootID\n\t// define other\n\tUrlStructure string `json:\"url_structure\" type:\"text\" required:\"true\" default:\"https://jsd.nn.ci/gh/alist-org/alist/README.md\\nhttps://jsd.nn.ci/gh/alist-org/alist/README_cn.md\\nfolder:\\n  CONTRIBUTING.md:1635:https://jsd.nn.ci/gh/alist-org/alist/CONTRIBUTING.md\\n  CODE_OF_CONDUCT.md:2093:https://jsd.nn.ci/gh/alist-org/alist/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\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           true,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"\",\n\tCheckStatus:       true,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\"io\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\tio.Reader\n}\n\nfunc (f DummyMFile) Read(p []byte) (n int, err error) {\n\treturn f.Reader.Read(p)\n}\n\nfunc (f DummyMFile) ReadAt(p []byte, off int64) (n int, err error) {\n\treturn f.Reader.Read(p)\n}\n\nfunc (f DummyMFile) Close() error {\n\treturn nil\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\tMFile: DummyMFile{Reader: random.Rand},\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tOnlyLocal: true,\n\tLocalSort: true,\n\tNeedMs:    true,\n\t//NoCache:   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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/vtencent/drive.go",
    "content": "package vtencent\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\ntype Vtencent struct {\n\tmodel.Storage\n\tAddition\n\tcron   *cron.Cron\n\tconfig driver.Config\n\tconf   Conf\n}\n\nfunc (d *Vtencent) Config() driver.Config {\n\treturn d.config\n}\n\nfunc (d *Vtencent) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Vtencent) Init(ctx context.Context) error {\n\ttfUid, err := d.LoadUser()\n\tif err != nil {\n\t\td.Status = err.Error()\n\t\top.MustSaveDriverStorage(d)\n\t\treturn nil\n\t}\n\td.Addition.TfUid = tfUid\n\top.MustSaveDriverStorage(d)\n\td.cron = cron.NewCron(time.Hour * 12)\n\td.cron.Do(func() {\n\t\t_, err := d.LoadUser()\n\t\tif err != nil {\n\t\t\td.Status = err.Error()\n\t\t\top.MustSaveDriverStorage(d)\n\t\t}\n\t})\n\treturn nil\n}\n\nfunc (d *Vtencent) Drop(ctx context.Context) error {\n\tif d.cron != nil {\n\t\td.cron.Stop()\n\t}\n\treturn nil\n}\n\nfunc (d *Vtencent) 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 *Vtencent) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tform := fmt.Sprintf(`{\"MaterialIds\":[\"%s\"]}`, file.GetID())\n\tvar dat map[string]interface{}\n\tif err := json.Unmarshal([]byte(form), &dat); err != nil {\n\t\treturn nil, err\n\t}\n\tvar resps RspDown\n\tapi := \"https://api.vs.tencent.com/SaaS/Material/DescribeMaterialDownloadUrl\"\n\trsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(dat)\n\t}, &resps)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := json.Unmarshal(rsp, &resps); err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resps.Data.DownloadURLInfoSet) == 0 {\n\t\treturn nil, err\n\t}\n\tu := resps.Data.DownloadURLInfoSet[0].DownloadURL\n\tlink := &model.Link{\n\t\tURL: u,\n\t\tHeader: http.Header{\n\t\t\t\"Referer\":    []string{d.conf.referer},\n\t\t\t\"User-Agent\": []string{d.conf.ua},\n\t\t},\n\t\tConcurrency: 2,\n\t\tPartSize:    10 * utils.MB,\n\t}\n\tif file.GetSize() == 0 {\n\t\tlink.Concurrency = 0\n\t\tlink.PartSize = 0\n\t}\n\treturn link, nil\n}\n\nfunc (d *Vtencent) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tclassId, err := strconv.Atoi(parentDir.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = d.request(\"https://api.vs.tencent.com/PaaS/Material/CreateClass\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(base.Json{\n\t\t\t\"Owner\": base.Json{\n\t\t\t\t\"Type\": \"PERSON\",\n\t\t\t\t\"Id\":   d.TfUid,\n\t\t\t},\n\t\t\t\"ParentClassId\": classId,\n\t\t\t\"Name\":          dirName,\n\t\t\t\"VerifySign\":    \"\"})\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Vtencent) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcType := \"MATERIAL\"\n\tif srcObj.IsDir() {\n\t\tsrcType = \"CLASS\"\n\t}\n\tform := fmt.Sprintf(`{\"SourceInfos\":[\n\t\t{\"Owner\":{\"Id\":\"%s\",\"Type\":\"PERSON\"},\n\t\t\"Resource\":{\"Type\":\"%s\",\"Id\":\"%s\"}}\n\t\t],\n\t\t\"Destination\":{\"Owner\":{\"Id\":\"%s\",\"Type\":\"PERSON\"},\n\t\t\"Resource\":{\"Type\":\"CLASS\",\"Id\":\"%s\"}}\n\t\t}`, d.TfUid, srcType, srcObj.GetID(), d.TfUid, dstDir.GetID())\n\tvar dat map[string]interface{}\n\tif err := json.Unmarshal([]byte(form), &dat); err != nil {\n\t\treturn err\n\t}\n\t_, err := d.request(\"https://api.vs.tencent.com/PaaS/Material/MoveResource\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(dat)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Vtencent) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tapi := \"https://api.vs.tencent.com/PaaS/Material/ModifyMaterial\"\n\tform := fmt.Sprintf(`{\n\t\t\"Owner\":{\"Type\":\"PERSON\",\"Id\":\"%s\"},\n\t\"MaterialId\":\"%s\",\"Name\":\"%s\"}`, d.TfUid, srcObj.GetID(), newName)\n\tif srcObj.IsDir() {\n\t\tclassId, err := strconv.Atoi(srcObj.GetID())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tapi = \"https://api.vs.tencent.com/PaaS/Material/ModifyClass\"\n\t\tform = fmt.Sprintf(`{\"Owner\":{\"Type\":\"PERSON\",\"Id\":\"%s\"},\n\t\"ClassId\":%d,\"Name\":\"%s\"}`, d.TfUid, classId, newName)\n\t}\n\tvar dat map[string]interface{}\n\tif err := json.Unmarshal([]byte(form), &dat); err != nil {\n\t\treturn err\n\t}\n\t_, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(dat)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Vtencent) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {\n\t// TODO copy obj, optional\n\treturn errs.NotImplement\n}\n\nfunc (d *Vtencent) Remove(ctx context.Context, obj model.Obj) error {\n\tsrcType := \"MATERIAL\"\n\tif obj.IsDir() {\n\t\tsrcType = \"CLASS\"\n\t}\n\tform := fmt.Sprintf(`{\n\t\t\"SourceInfos\":[\n\t\t\t{\"Owner\":{\"Type\":\"PERSON\",\"Id\":\"%s\"},\n\t\t\t\"Resource\":{\"Type\":\"%s\",\"Id\":\"%s\"}}\n\t\t\t]\n\t\t}`, d.TfUid, srcType, obj.GetID())\n\tvar dat map[string]interface{}\n\tif err := json.Unmarshal([]byte(form), &dat); err != nil {\n\t\treturn err\n\t}\n\t_, err := d.request(\"https://api.vs.tencent.com/PaaS/Material/DeleteResource\", http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(dat)\n\t}, nil)\n\treturn err\n}\n\nfunc (d *Vtencent) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\terr := d.FileUpload(ctx, dstDir, stream, up)\n\treturn err\n}\n\n//func (d *Vtencent) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {\n//\treturn nil, errs.NotSupport\n//}\n\nvar _ driver.Driver = (*Vtencent)(nil)\n"
  },
  {
    "path": "drivers/vtencent/meta.go",
    "content": "package vtencent\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tCookie         string `json:\"cookie\" required:\"true\"`\n\tTfUid          string `json:\"tf_uid\"`\n\tOrderBy        string `json:\"order_by\" type:\"select\" options:\"Name,Size,UpdateTime,CreatTime\"`\n\tOrderDirection string `json:\"order_direction\" type:\"select\" options:\"Asc,Desc\"`\n}\n\ntype Conf struct {\n\tua      string\n\treferer string\n\torigin  string\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Vtencent{\n\t\t\tconfig: driver.Config{\n\t\t\t\tName:              \"VTencent\",\n\t\t\t\tOnlyProxy:         true,\n\t\t\t\tOnlyLocal:         false,\n\t\t\t\tDefaultRoot:       \"9\",\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://app.v.tencent.com/\",\n\t\t\t\torigin:  \"https://app.v.tencent.com\",\n\t\t\t},\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "drivers/vtencent/signature.go",
    "content": "package vtencent\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n)\n\nfunc QSignatureKey(timeKey string, signPath string, key string) string {\n\tsignKey := hmac.New(sha1.New, []byte(key))\n\tsignKey.Write([]byte(timeKey))\n\tsignKeyBytes := signKey.Sum(nil)\n\tsignKeyHex := hex.EncodeToString(signKeyBytes)\n\tsha := sha1.New()\n\tsha.Write([]byte(signPath))\n\tshaBytes := sha.Sum(nil)\n\tshaHex := hex.EncodeToString(shaBytes)\n\n\tO := \"sha1\\n\" + timeKey + \"\\n\" + shaHex + \"\\n\"\n\tdataSignKey := hmac.New(sha1.New, []byte(signKeyHex))\n\tdataSignKey.Write([]byte(O))\n\tdataSignKeyBytes := dataSignKey.Sum(nil)\n\tdataSignKeyHex := hex.EncodeToString(dataSignKeyBytes)\n\treturn dataSignKeyHex\n}\n\nfunc QTwoSignatureKey(timeKey string, key string) string {\n\tsignKey := hmac.New(sha1.New, []byte(key))\n\tsignKey.Write([]byte(timeKey))\n\tsignKeyBytes := signKey.Sum(nil)\n\tsignKeyHex := hex.EncodeToString(signKeyBytes)\n\treturn signKeyHex\n}\n"
  },
  {
    "path": "drivers/vtencent/types.go",
    "content": "package vtencent\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n)\n\ntype RespErr struct {\n\tCode    string `json:\"Code\"`\n\tMessage string `json:\"Message\"`\n}\n\ntype Reqfiles struct {\n\tScrollToken string `json:\"ScrollToken\"`\n\tText        string `json:\"Text\"`\n\tOffset      int    `json:\"Offset\"`\n\tLimit       int    `json:\"Limit\"`\n\tSort        struct {\n\t\tField string `json:\"Field\"`\n\t\tOrder string `json:\"Order\"`\n\t} `json:\"Sort\"`\n\tCreateTimeRanges []any `json:\"CreateTimeRanges\"`\n\tMaterialTypes    []any `json:\"MaterialTypes\"`\n\tReviewStatuses   []any `json:\"ReviewStatuses\"`\n\tTags             []any `json:\"Tags\"`\n\tSearchScopes     []struct {\n\t\tOwner struct {\n\t\t\tType string `json:\"Type\"`\n\t\t\tID   string `json:\"Id\"`\n\t\t} `json:\"Owner\"`\n\t\tClassID        int  `json:\"ClassId\"`\n\t\tSearchOneDepth bool `json:\"SearchOneDepth\"`\n\t} `json:\"SearchScopes\"`\n}\n\ntype File struct {\n\tType      string `json:\"Type\"`\n\tClassInfo struct {\n\t\tClassID     int       `json:\"ClassId\"`\n\t\tName        string    `json:\"Name\"`\n\t\tUpdateTime  time.Time `json:\"UpdateTime\"`\n\t\tCreateTime  time.Time `json:\"CreateTime\"`\n\t\tFileInboxID string    `json:\"FileInboxId\"`\n\t\tOwner       struct {\n\t\t\tType string `json:\"Type\"`\n\t\t\tID   string `json:\"Id\"`\n\t\t} `json:\"Owner\"`\n\t\tClassPath      string `json:\"ClassPath\"`\n\t\tParentClassID  int    `json:\"ParentClassId\"`\n\t\tAttachmentInfo struct {\n\t\t\tSubClassCount int   `json:\"SubClassCount\"`\n\t\t\tMaterialCount int   `json:\"MaterialCount\"`\n\t\t\tSize          int64 `json:\"Size\"`\n\t\t} `json:\"AttachmentInfo\"`\n\t\tClassPreviewURLSet []string `json:\"ClassPreviewUrlSet\"`\n\t} `json:\"ClassInfo\"`\n\tMaterialInfo struct {\n\t\tBasicInfo struct {\n\t\t\tMaterialID             string    `json:\"MaterialId\"`\n\t\t\tMaterialType           string    `json:\"MaterialType\"`\n\t\t\tName                   string    `json:\"Name\"`\n\t\t\tCreateTime             time.Time `json:\"CreateTime\"`\n\t\t\tUpdateTime             time.Time `json:\"UpdateTime\"`\n\t\t\tClassPath              string    `json:\"ClassPath\"`\n\t\t\tClassID                int       `json:\"ClassId\"`\n\t\t\tTagInfoSet             []any     `json:\"TagInfoSet\"`\n\t\t\tTagSet                 []any     `json:\"TagSet\"`\n\t\t\tPreviewURL             string    `json:\"PreviewUrl\"`\n\t\t\tMediaURL               string    `json:\"MediaUrl\"`\n\t\t\tUnifiedMediaPreviewURL string    `json:\"UnifiedMediaPreviewUrl\"`\n\t\t\tOwner                  struct {\n\t\t\t\tType string `json:\"Type\"`\n\t\t\t\tID   string `json:\"Id\"`\n\t\t\t} `json:\"Owner\"`\n\t\t\tPermissionSet        any    `json:\"PermissionSet\"`\n\t\t\tPermissionInfoSet    []any  `json:\"PermissionInfoSet\"`\n\t\t\tTfUID                string `json:\"TfUid\"`\n\t\t\tGroupID              string `json:\"GroupId\"`\n\t\t\tVersionMaterialIDSet []any  `json:\"VersionMaterialIdSet\"`\n\t\t\tFileType             string `json:\"FileType\"`\n\t\t\tCmeMaterialPlayList  []any  `json:\"CmeMaterialPlayList\"`\n\t\t\tStatus               string `json:\"Status\"`\n\t\t\tDownloadSwitch       string `json:\"DownloadSwitch\"`\n\t\t} `json:\"BasicInfo\"`\n\t\tMediaInfo struct {\n\t\t\tWidth          int     `json:\"Width\"`\n\t\t\tHeight         int     `json:\"Height\"`\n\t\t\tSize           int     `json:\"Size\"`\n\t\t\tDuration       float64 `json:\"Duration\"`\n\t\t\tFps            int     `json:\"Fps\"`\n\t\t\tBitRate        int     `json:\"BitRate\"`\n\t\t\tCodec          string  `json:\"Codec\"`\n\t\t\tMediaType      string  `json:\"MediaType\"`\n\t\t\tFavoriteStatus string  `json:\"FavoriteStatus\"`\n\t\t} `json:\"MediaInfo\"`\n\t\tMaterialStatus struct {\n\t\t\tContentReviewStatus          string `json:\"ContentReviewStatus\"`\n\t\t\tEditorUsableStatus           string `json:\"EditorUsableStatus\"`\n\t\t\tUnifiedPreviewStatus         string `json:\"UnifiedPreviewStatus\"`\n\t\t\tEditPreviewImageSpiritStatus string `json:\"EditPreviewImageSpiritStatus\"`\n\t\t\tTranscodeStatus              string `json:\"TranscodeStatus\"`\n\t\t\tAdaptiveStreamingStatus      string `json:\"AdaptiveStreamingStatus\"`\n\t\t\tStreamConnectable            string `json:\"StreamConnectable\"`\n\t\t\tAiAnalysisStatus             string `json:\"AiAnalysisStatus\"`\n\t\t\tAiRecognitionStatus          string `json:\"AiRecognitionStatus\"`\n\t\t} `json:\"MaterialStatus\"`\n\t\tImageMaterial struct {\n\t\t\tHeight      int    `json:\"Height\"`\n\t\t\tWidth       int    `json:\"Width\"`\n\t\t\tSize        int    `json:\"Size\"`\n\t\t\tMaterialURL string `json:\"MaterialUrl\"`\n\t\t\tResolution  string `json:\"Resolution\"`\n\t\t\tVodFileID   string `json:\"VodFileId\"`\n\t\t\tOriginalURL string `json:\"OriginalUrl\"`\n\t\t} `json:\"ImageMaterial\"`\n\t\tVideoMaterial struct {\n\t\t\tMetaData struct {\n\t\t\t\tSize               int     `json:\"Size\"`\n\t\t\t\tContainer          string  `json:\"Container\"`\n\t\t\t\tBitrate            int     `json:\"Bitrate\"`\n\t\t\t\tHeight             int     `json:\"Height\"`\n\t\t\t\tWidth              int     `json:\"Width\"`\n\t\t\t\tDuration           float64 `json:\"Duration\"`\n\t\t\t\tRotate             int     `json:\"Rotate\"`\n\t\t\t\tVideoStreamInfoSet []struct {\n\t\t\t\t\tBitrate int    `json:\"Bitrate\"`\n\t\t\t\t\tHeight  int    `json:\"Height\"`\n\t\t\t\t\tWidth   int    `json:\"Width\"`\n\t\t\t\t\tCodec   string `json:\"Codec\"`\n\t\t\t\t\tFps     int    `json:\"Fps\"`\n\t\t\t\t} `json:\"VideoStreamInfoSet\"`\n\t\t\t\tAudioStreamInfoSet []struct {\n\t\t\t\t\tBitrate      int    `json:\"Bitrate\"`\n\t\t\t\t\tSamplingRate int    `json:\"SamplingRate\"`\n\t\t\t\t\tCodec        string `json:\"Codec\"`\n\t\t\t\t} `json:\"AudioStreamInfoSet\"`\n\t\t\t} `json:\"MetaData\"`\n\t\t\tImageSpriteInfo    any    `json:\"ImageSpriteInfo\"`\n\t\t\tMaterialURL        string `json:\"MaterialUrl\"`\n\t\t\tCoverURL           string `json:\"CoverUrl\"`\n\t\t\tResolution         string `json:\"Resolution\"`\n\t\t\tVodFileID          string `json:\"VodFileId\"`\n\t\t\tOriginalURL        string `json:\"OriginalUrl\"`\n\t\t\tAudioWaveformURL   string `json:\"AudioWaveformUrl\"`\n\t\t\tSubtitleURL        string `json:\"SubtitleUrl\"`\n\t\t\tTranscodeInfoSet   []any  `json:\"TranscodeInfoSet\"`\n\t\t\tImageSpriteInfoSet []any  `json:\"ImageSpriteInfoSet\"`\n\t\t} `json:\"VideoMaterial\"`\n\t} `json:\"MaterialInfo\"`\n}\n\ntype RspFiles struct {\n\tCode           string `json:\"Code\"`\n\tMessage        string `json:\"Message\"`\n\tEnglishMessage string `json:\"EnglishMessage\"`\n\tData           struct {\n\t\tTotalCount      int    `json:\"TotalCount\"`\n\t\tResourceInfoSet []File `json:\"ResourceInfoSet\"`\n\t\tScrollToken     string `json:\"ScrollToken\"`\n\t} `json:\"Data\"`\n}\n\ntype RspDown struct {\n\tCode           string `json:\"Code\"`\n\tMessage        string `json:\"Message\"`\n\tEnglishMessage string `json:\"EnglishMessage\"`\n\tData           struct {\n\t\tDownloadURLInfoSet []struct {\n\t\t\tMaterialID  string `json:\"MaterialId\"`\n\t\t\tDownloadURL string `json:\"DownloadUrl\"`\n\t\t} `json:\"DownloadUrlInfoSet\"`\n\t} `json:\"Data\"`\n}\n\ntype RspCreatrMaterial struct {\n\tCode           string `json:\"Code\"`\n\tMessage        string `json:\"Message\"`\n\tEnglishMessage string `json:\"EnglishMessage\"`\n\tData           struct {\n\t\tUploadContext string `json:\"UploadContext\"`\n\t\tVodUploadSign string `json:\"VodUploadSign\"`\n\t\tQuickUpload   bool   `json:\"QuickUpload\"`\n\t} `json:\"Data\"`\n}\n\ntype RspApplyUploadUGC struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tVideo struct {\n\t\t\tStorageSignature string `json:\"storageSignature\"`\n\t\t\tStoragePath      string `json:\"storagePath\"`\n\t\t} `json:\"video\"`\n\t\tStorageAppID    int    `json:\"storageAppId\"`\n\t\tStorageBucket   string `json:\"storageBucket\"`\n\t\tStorageRegion   string `json:\"storageRegion\"`\n\t\tStorageRegionV5 string `json:\"storageRegionV5\"`\n\t\tDomain          string `json:\"domain\"`\n\t\tVodSessionKey   string `json:\"vodSessionKey\"`\n\t\tTempCertificate struct {\n\t\t\tSecretID    string `json:\"secretId\"`\n\t\t\tSecretKey   string `json:\"secretKey\"`\n\t\t\tToken       string `json:\"token\"`\n\t\t\tExpiredTime int    `json:\"expiredTime\"`\n\t\t} `json:\"tempCertificate\"`\n\t\tAppID                     int    `json:\"appId\"`\n\t\tTimestamp                 int    `json:\"timestamp\"`\n\t\tStorageRegionV50          string `json:\"StorageRegionV5\"`\n\t\tMiniProgramAccelerateHost string `json:\"MiniProgramAccelerateHost\"`\n\t} `json:\"data\"`\n}\n\ntype RspCommitUploadUGC struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tVideo struct {\n\t\t\tURL           string `json:\"url\"`\n\t\t\tVerifyContent string `json:\"verify_content\"`\n\t\t} `json:\"video\"`\n\t\tFileID string `json:\"fileId\"`\n\t} `json:\"data\"`\n}\n\ntype RspFinishUpload struct {\n\tCode           string `json:\"Code\"`\n\tMessage        string `json:\"Message\"`\n\tEnglishMessage string `json:\"EnglishMessage\"`\n\tData           struct {\n\t\tMaterialID string `json:\"MaterialId\"`\n\t} `json:\"Data\"`\n}\n\nfunc fileToObj(f File) *model.Object {\n\tobj := &model.Object{}\n\tif f.Type == \"CLASS\" {\n\t\tobj.Name = f.ClassInfo.Name\n\t\tobj.ID = strconv.Itoa(f.ClassInfo.ClassID)\n\t\tobj.IsFolder = true\n\t\tobj.Modified = f.ClassInfo.CreateTime\n\t\tobj.Size = 0\n\t} else if f.Type == \"MATERIAL\" {\n\t\tobj.Name = f.MaterialInfo.BasicInfo.Name\n\t\tobj.ID = f.MaterialInfo.BasicInfo.MaterialID\n\t\tobj.IsFolder = false\n\t\tobj.Modified = f.MaterialInfo.BasicInfo.CreateTime\n\t\tobj.Size = int64(f.MaterialInfo.MediaInfo.Size)\n\t}\n\treturn obj\n}\n"
  },
  {
    "path": "drivers/vtencent/util.go",
    "content": "package vtencent\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/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)\n\nfunc (d *Vtencent) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"cookie\":       d.Cookie,\n\t\t\"content-type\": \"application/json\",\n\t\t\"origin\":       d.conf.origin,\n\t\t\"referer\":      d.conf.referer,\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\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := utils.Json.Get(res.Body(), \"Code\").ToString()\n\tif code != \"Success\" {\n\t\tswitch code {\n\t\tcase \"AuthFailure.SessionInvalid\":\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(code)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errors.New(code)\n\t\t}\n\t\treturn d.request(url, method, callback, resp)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Vtencent) ugcRequest(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {\n\treq := base.RestyClient.R()\n\treq.SetHeaders(map[string]string{\n\t\t\"cookie\":       d.Cookie,\n\t\t\"content-type\": \"application/json\",\n\t\t\"origin\":       d.conf.origin,\n\t\t\"referer\":      d.conf.referer,\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\tres, err := req.Execute(method, url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcode := utils.Json.Get(res.Body(), \"Code\").ToInt()\n\tif code != 0 {\n\t\tmessage := utils.Json.Get(res.Body(), \"message\").ToString()\n\t\tif len(message) == 0 {\n\t\t\tmessage = utils.Json.Get(res.Body(), \"msg\").ToString()\n\t\t}\n\t\treturn nil, errors.New(message)\n\t}\n\treturn res.Body(), nil\n}\n\nfunc (d *Vtencent) LoadUser() (string, error) {\n\tapi := \"https://api.vs.tencent.com/SaaS/Account/DescribeAccount\"\n\tres, err := d.request(api, http.MethodPost, func(req *resty.Request) {}, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn utils.Json.Get(res, \"Data\", \"TfUid\").ToString(), nil\n}\n\nfunc (d *Vtencent) GetFiles(dirId string) ([]File, error) {\n\tvar res []File\n\t//offset := 0\n\tfor {\n\t\tapi := \"https://api.vs.tencent.com/PaaS/Material/SearchResource\"\n\t\tform := fmt.Sprintf(`{\n\t\t\"Text\":\"\",\n\t\t\"Text\":\"\",\n\t\t\"Offset\":%d,\n\t\t\"Limit\":50,\n\t\t\"Sort\":{\"Field\":\"%s\",\"Order\":\"%s\"},\n\t\t\"CreateTimeRanges\":[],\n\t\t\"MaterialTypes\":[],\n\t\t\"ReviewStatuses\":[],\n\t\t\"Tags\":[],\n\t\t\"SearchScopes\":[{\"Owner\":{\"Type\":\"PERSON\",\"Id\":\"%s\"},\"ClassId\":%s,\"SearchOneDepth\":true}]\n\t}`, len(res), d.Addition.OrderBy, d.Addition.OrderDirection, d.TfUid, dirId)\n\t\tvar resp RspFiles\n\t\t_, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t\t}, &resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, resp.Data.ResourceInfoSet...)\n\t\tif len(resp.Data.ResourceInfoSet) <= 0 || len(res) >= resp.Data.TotalCount {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (d *Vtencent) CreateUploadMaterial(classId int, fileName string, UploadSummaryKey string) (RspCreatrMaterial, error) {\n\tapi := \"https://api.vs.tencent.com/PaaS/Material/CreateUploadMaterial\"\n\tform := base.Json{\"Owner\": base.Json{\"Type\": \"PERSON\", \"Id\": d.TfUid},\n\t\t\"MaterialType\": \"VIDEO\", \"Name\": fileName, \"ClassId\": classId,\n\t\t\"UploadSummaryKey\": UploadSummaryKey}\n\tvar resps RspCreatrMaterial\n\t_, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t}, &resps)\n\tif err != nil {\n\t\treturn RspCreatrMaterial{}, err\n\t}\n\treturn resps, nil\n}\n\nfunc (d *Vtencent) ApplyUploadUGC(signature string, stream model.FileStreamer) (RspApplyUploadUGC, error) {\n\tapi := \"https://vod2.qcloud.com/v3/index.php?Action=ApplyUploadUGC\"\n\tform := base.Json{\n\t\t\"signature\": signature,\n\t\t\"videoName\": stream.GetName(),\n\t\t\"videoType\": utils.Ext(stream.GetName()),\n\t\t\"videoSize\": stream.GetSize(),\n\t}\n\tvar resps RspApplyUploadUGC\n\t_, err := d.ugcRequest(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t}, &resps)\n\tif err != nil {\n\t\treturn RspApplyUploadUGC{}, err\n\t}\n\treturn resps, nil\n}\n\nfunc (d *Vtencent) CommitUploadUGC(signature string, vodSessionKey string) (RspCommitUploadUGC, error) {\n\tapi := \"https://vod2.qcloud.com/v3/index.php?Action=CommitUploadUGC\"\n\tform := base.Json{\n\t\t\"signature\":     signature,\n\t\t\"vodSessionKey\": vodSessionKey,\n\t}\n\tvar resps RspCommitUploadUGC\n\trsp, err := d.ugcRequest(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t}, &resps)\n\tif err != nil {\n\t\treturn RspCommitUploadUGC{}, err\n\t}\n\tif len(resps.Data.Video.URL) == 0 {\n\t\treturn RspCommitUploadUGC{}, errors.New(string(rsp))\n\t}\n\treturn resps, nil\n}\n\nfunc (d *Vtencent) FinishUploadMaterial(SummaryKey string, VodVerifyKey string, UploadContext, VodFileId string) (RspFinishUpload, error) {\n\tapi := \"https://api.vs.tencent.com/PaaS/Material/FinishUploadMaterial\"\n\tform := base.Json{\n\t\t\"UploadContext\": UploadContext,\n\t\t\"VodVerifyKey\":  VodVerifyKey,\n\t\t\"VodFileId\":     VodFileId,\n\t\t\"UploadFullKey\": SummaryKey}\n\tvar resps RspFinishUpload\n\trsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t}, &resps)\n\tif err != nil {\n\t\treturn RspFinishUpload{}, err\n\t}\n\tif len(resps.Data.MaterialID) == 0 {\n\t\treturn RspFinishUpload{}, errors.New(string(rsp))\n\t}\n\treturn resps, nil\n}\n\nfunc (d *Vtencent) FinishHashUploadMaterial(SummaryKey string, UploadContext string) (RspFinishUpload, error) {\n\tapi := \"https://api.vs.tencent.com/PaaS/Material/FinishUploadMaterial\"\n\tvar resps RspFinishUpload\n\tform := base.Json{\n\t\t\"UploadContext\": UploadContext,\n\t\t\"UploadFullKey\": SummaryKey}\n\trsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {\n\t\treq.SetBody(form).ForceContentType(\"application/json\")\n\t}, &resps)\n\tif err != nil {\n\t\treturn RspFinishUpload{}, err\n\t}\n\tif len(resps.Data.MaterialID) == 0 {\n\t\treturn RspFinishUpload{}, errors.New(string(rsp))\n\t}\n\treturn resps, nil\n}\n\nfunc (d *Vtencent) FileUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {\n\tclassId, err := strconv.Atoi(dstDir.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\tconst chunkLength int64 = 1024 * 1024 * 10\n\treader, err := stream.RangeRead(http_range.Range{Start: 0, Length: chunkLength})\n\tif err != nil {\n\t\treturn err\n\t}\n\tchunkHash, err := utils.HashReader(utils.SHA1, reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\trspCreatrMaterial, err := d.CreateUploadMaterial(classId, stream.GetName(), chunkHash)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif rspCreatrMaterial.Data.QuickUpload {\n\t\tSummaryKey := stream.GetHash().GetHash(utils.SHA1)\n\t\tif len(SummaryKey) < utils.SHA1.Width {\n\t\t\tif SummaryKey, err = utils.HashReader(utils.SHA1, stream); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tUploadContext := rspCreatrMaterial.Data.UploadContext\n\t\t_, err = d.FinishHashUploadMaterial(SummaryKey, UploadContext)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\thash := sha1.New()\n\trspUGC, err := d.ApplyUploadUGC(rspCreatrMaterial.Data.VodUploadSign, stream)\n\tif err != nil {\n\t\treturn err\n\t}\n\tparams := rspUGC.Data\n\tcertificate := params.TempCertificate\n\tcfg := &aws.Config{\n\t\tHTTPClient: base.HttpClient,\n\t\t// S3ForcePathStyle: aws.Bool(true),\n\t\tCredentials: credentials.NewStaticCredentials(certificate.SecretID, certificate.SecretKey, certificate.Token),\n\t\tRegion:      aws.String(params.StorageRegionV5),\n\t\tEndpoint:    aws.String(fmt.Sprintf(\"cos.%s.myqcloud.com\", params.StorageRegionV5)),\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: aws.String(fmt.Sprintf(\"%s-%d\", params.StorageBucket, params.StorageAppID)),\n\t\tKey:    &params.Video.StoragePath,\n\t\tBody: driver.NewLimitedUploadStream(ctx,\n\t\t\tio.TeeReader(stream, io.MultiWriter(hash, driver.NewProgress(stream.GetSize(), up)))),\n\t}\n\t_, err = uploader.UploadWithContext(ctx, input)\n\tif err != nil {\n\t\treturn err\n\t}\n\trspCommitUGC, err := d.CommitUploadUGC(rspCreatrMaterial.Data.VodUploadSign, rspUGC.Data.VodSessionKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tVodVerifyKey := rspCommitUGC.Data.Video.VerifyContent\n\tVodFileId := rspCommitUGC.Data.FileID\n\tUploadContext := rspCreatrMaterial.Data.UploadContext\n\tSummaryKey := hex.EncodeToString(hash.Sum(nil))\n\t_, err = d.FinishUploadMaterial(SummaryKey, VodVerifyKey, UploadContext, VodFileId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/webdav/driver.go",
    "content": "package webdav\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/gowebdav\"\n\t\"github.com/alist-org/alist/v3/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\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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}\n\nvar config = driver.Config{\n\tName:        \"WebDav\",\n\tLocalSort:   true,\n\tOnlyProxy:   true,\n\tDefaultRoot: \"/\",\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/alist-org/alist/v3/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/alist-org/alist/v3/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(\"POST\", 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/alist-org/alist/v3/drivers/webdav/odrvcookie\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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: conf.Conf.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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/cron\"\n\t\"github.com/alist-org/alist/v3/pkg/errgroup\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/avast/retry-go\"\n\tweiyunsdkgo \"github.com/foxxorcat/weiyun-sdk-go\"\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() == 1 {\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.CacheFullInTempFile()\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\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)\n"
  },
  {
    "path": "drivers/weiyun/meta.go",
    "content": "package weiyun\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tLocalSort:         false,\n\tOnlyProxy:         true,\n\tCheckStatus:       true,\n\tAlert:             \"\",\n\tNoOverwriteUpload: false,\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\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"time\"\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/xhofe/wopan-sdk-go\"\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\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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/xhofe/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/xhofe/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\treturn time.Parse(\"20060102150405\", str)\n}\n"
  },
  {
    "path": "drivers/wukong/driver.go",
    "content": "package wukong\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\tcrand \"crypto/rand\"\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\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst (\n\twukongBaseURL          = \"https://api.wkbrowser.com\"\n\twebReferer             = \"https://pan.wkbrowser.com/\"\n\tvodBaseURL             = \"https://vod.bytedanceapi.com\"\n\tvodRegion              = \"cn-north-1\"\n\tvodService             = \"vod\"\n\tvideoSpaceName         = \"wukong_netdisk_ugc\"\n\tminUploadSubmitSuccess = 2000\n\tmultipartChunkSize     = int64(5 * 1024 * 1024)\n)\n\ntype Wukong struct {\n\tmodel.Storage\n\tAddition\n\tclient *resty.Client\n}\n\nfunc (d *Wukong) Config() driver.Config {\n\treturn config\n}\n\nfunc (d *Wukong) GetAddition() driver.Additional {\n\treturn &d.Addition\n}\n\nfunc (d *Wukong) Init(ctx context.Context) error {\n\td.client = base.NewRestyClient().\n\t\tSetBaseURL(wukongBaseURL).\n\t\tSetHeader(\"accept\", \"application/json, text/plain, */*\").\n\t\tSetHeader(\"content-type\", \"application/json\").\n\t\tSetHeader(\"referer\", webReferer).\n\t\tSetHeader(\"origin\", \"https://pan.wkbrowser.com\")\n\tif d.Cookie != \"\" {\n\t\td.client.SetHeader(\"cookie\", d.Cookie)\n\t}\n\tif d.RootFolderID == \"\" {\n\t\td.RootFolderID = \"0\"\n\t}\n\tif strings.TrimSpace(d.Aid) == \"\" {\n\t\td.Aid = \"590353\"\n\t}\n\tif strings.TrimSpace(d.Language) == \"\" {\n\t\td.Language = \"zh\"\n\t}\n\tif d.PageSize <= 0 {\n\t\td.PageSize = 100\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) Drop(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Wukong) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {\n\tfatherID := dir.GetID()\n\tif fatherID == \"\" {\n\t\tfatherID = d.RootFolderID\n\t}\n\toffset := 0\n\tlimit := d.PageSize\n\tobjs := make([]model.Obj, 0)\n\tfor {\n\t\tvar resp filterFileResp\n\t\t_, err := d.client.R().\n\t\t\tSetContext(ctx).\n\t\t\tSetQueryParams(map[string]string{\n\t\t\t\t\"offset\":          strconv.Itoa(offset),\n\t\t\t\t\"limit\":           strconv.Itoa(limit),\n\t\t\t\t\"aid\":             d.Aid,\n\t\t\t\t\"device_platform\": \"web\",\n\t\t\t\t\"language\":        d.Language,\n\t\t\t}).\n\t\t\tSetBody(map[string]any{\n\t\t\t\t\"father_id\":   asIDValue(fatherID),\n\t\t\t\t\"filter_type\": 2,\n\t\t\t\t\"is_desc\":     1,\n\t\t\t\t\"file_type\":   0,\n\t\t\t}).\n\t\t\tSetResult(&resp).\n\t\t\tPost(\"/netdisk/user_file/filter_file\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resp.Code != 0 {\n\t\t\treturn nil, fmt.Errorf(\"wukong list failed: code=%d message=%s\", resp.Code, resp.Message)\n\t\t}\n\n\t\tfor _, item := range resp.Data.FileList {\n\t\t\tobjs = append(objs, &model.Object{\n\t\t\t\tID:       strconv.FormatInt(item.FileID, 10),\n\t\t\t\tPath:     strconv.FormatInt(item.FatherID, 10),\n\t\t\t\tName:     item.FileName,\n\t\t\t\tSize:     item.Size,\n\t\t\t\tModified: parseUnix(item.UpdatedAt),\n\t\t\t\tCtime:    parseUnix(item.CreatedAt),\n\t\t\t\tIsFolder: item.IsDirectory == 1,\n\t\t\t})\n\t\t}\n\n\t\tif !hasMore(resp.Data.HasMore) || len(resp.Data.FileList) == 0 {\n\t\t\tbreak\n\t\t}\n\t\toffset += len(resp.Data.FileList)\n\t}\n\treturn objs, nil\n}\n\nfunc (d *Wukong) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {\n\tif file.IsDir() {\n\t\treturn nil, errs.NotFile\n\t}\n\tfileID := file.GetID()\n\tif fileID == \"\" {\n\t\treturn nil, errors.New(\"missing file id\")\n\t}\n\n\tvar resp rawResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(map[string]any{\n\t\t\t\"file_id_list\": []any{asIDValue(fileID)},\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/user_file/detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"wukong detail failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\n\turl := extractDetailMainURL(resp.Data)\n\tif url == \"\" {\n\t\turl = extractURL(resp.Data)\n\t}\n\tif url == \"\" {\n\t\treturn nil, errs.NotImplement\n\t}\n\n\treturn &model.Link{\n\t\tURL: url,\n\t\tHeader: http.Header{\n\t\t\t\"Referer\": []string{webReferer},\n\t\t\t\"Cookie\":  []string{d.Cookie},\n\t\t},\n\t}, nil\n}\n\nfunc (d *Wukong) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {\n\tfatherID := parentDir.GetID()\n\tif fatherID == \"\" {\n\t\tfatherID = d.RootFolderID\n\t}\n\n\tvar resp rawResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(map[string]any{\n\t\t\t\"father_id\": asIDValue(fatherID),\n\t\t\t\"file_name\": dirName,\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/user_file/create_directory\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 0 {\n\t\treturn fmt.Errorf(\"wukong create directory failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) Move(ctx context.Context, srcObj, dstDir model.Obj) error {\n\tsrcID := srcObj.GetID()\n\tif srcID == \"\" {\n\t\treturn errors.New(\"missing source file id\")\n\t}\n\n\tdstID := dstDir.GetID()\n\tif dstID == \"\" {\n\t\tdstID = d.RootFolderID\n\t}\n\n\tvar resp rawResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(map[string]any{\n\t\t\t\"file_id_list\":  []any{asIDValue(srcID)},\n\t\t\t\"new_father_id\": asIDValue(dstID),\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/user_file/move_file\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 0 {\n\t\treturn fmt.Errorf(\"wukong move failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) Rename(ctx context.Context, srcObj model.Obj, newName string) error {\n\tsrcID := srcObj.GetID()\n\tif srcID == \"\" {\n\t\treturn errors.New(\"missing file id\")\n\t}\n\n\tvar resp rawResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(map[string]any{\n\t\t\t\"file_id\":  asIDValue(srcID),\n\t\t\t\"new_name\": newName,\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/user_file/rename_file\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 0 {\n\t\treturn fmt.Errorf(\"wukong rename failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) Remove(ctx context.Context, obj model.Obj) error {\n\tfileID := obj.GetID()\n\tif fileID == \"\" {\n\t\treturn errors.New(\"missing file id\")\n\t}\n\n\tvar resp rawResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(map[string]any{\n\t\t\t\"file_id_list\": []any{asIDValue(fileID)},\n\t\t}).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/user_file/delete_file\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 0 {\n\t\treturn fmt.Errorf(\"wukong delete failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {\n\tfatherID := dstDir.GetID()\n\tif fatherID == \"\" {\n\t\tfatherID = d.RootFolderID\n\t}\n\n\ttempFile, err := file.CacheFullInTempFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tempFile.Close()\n\tif _, err = tempFile.Seek(0, io.SeekStart); err != nil {\n\t\treturn err\n\t}\n\n\tmd5Hex, crc32Hex, err := calcFileMD5AndCRC32(tempFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\text := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.GetName())), \".\")\n\tfileType := detectWukongFileType(file.GetMimetype(), file.GetName())\n\tsize := file.GetSize()\n\tup(5)\n\n\tuploadType := detectUploadType(file.GetMimetype(), file.GetName())\n\tauthToken, err := d.getUploadAuthToken(ctx, uploadType)\n\tif err != nil {\n\t\treturn err\n\t}\n\tup(10)\n\n\tcandidates, err := d.getUploadCandidates(ctx, authToken)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbestHosts := collectCandidateHosts(candidates)\n\tif len(bestHosts) == 0 {\n\t\treturn errors.New(\"wukong upload candidates is empty\")\n\t}\n\tup(20)\n\n\tapplyResp, err := d.applyUploadInner(ctx, authToken, uploadType, size, strings.Join(bestHosts, \",\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(applyResp.Result.InnerUploadAddress.UploadNodes) == 0 ||\n\t\tlen(applyResp.Result.InnerUploadAddress.UploadNodes[0].StoreInfos) == 0 {\n\t\treturn errors.New(\"wukong apply upload inner returns empty upload node\")\n\t}\n\tnode := applyResp.Result.InnerUploadAddress.UploadNodes[0]\n\tstore := node.StoreInfos[0]\n\tup(30)\n\n\tif size > multipartChunkSize {\n\t\tif err = d.uploadToTOSMultipart(ctx, node.UploadHost, store.StoreURI, store.Auth, getStorageUserID(store.StorageHeader), tempFile, size, up); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tif _, err = tempFile.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treader := &driver.ReaderUpdatingProgress{\n\t\t\tReader: &driver.SimpleReaderWithSize{Reader: tempFile, Size: size},\n\t\t\tUpdateProgress: func(percent float64) {\n\t\t\t\tup(30 + percent*0.5)\n\t\t\t},\n\t\t}\n\t\tif err = d.uploadToTOS(ctx, node.UploadHost, store.StoreURI, store.Auth, getStorageUserID(store.StorageHeader), crc32Hex, file.GetName(), reader, size); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tup(85)\n\n\tvideoVid, err := d.commitUploadInner(ctx, authToken, chooseCommitSpace(uploadType, authToken.SpaceName), node.SessionKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tup(92)\n\n\tif fileType == 3000 && videoVid == \"\" {\n\t\treturn errors.New(\"wukong video upload missing vid in commit response\")\n\t}\n\tif err = d.uploadSubmit(ctx, fatherID, file.GetName(), ext, fileType, size, md5Hex, store.StoreURI, videoVid); err != nil {\n\t\treturn err\n\t}\n\tup(100)\n\treturn nil\n}\n\nfunc (d *Wukong) getUploadAuthToken(ctx context.Context, uploadType string) (*uploadAuthTokenResp, error) {\n\tvar resp uploadAuthTokenResp\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"upload_source\":   uploadSourceByType(uploadType),\n\t\t\t\"type\":            uploadType,\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetResult(&resp).\n\t\tGet(\"/toutiao/upload/auth_token/v1/\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"wukong get upload auth token failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Wukong) getUploadCandidates(ctx context.Context, auth *uploadAuthTokenResp) (*getUploadCandidatesResp, error) {\n\tq := map[string]string{\n\t\t\"Action\":    \"GetUploadCandidates\",\n\t\t\"Version\":   \"2020-11-19\",\n\t\t\"SpaceName\": videoSpaceName,\n\t}\n\tvar resp getUploadCandidatesResp\n\tif err := d.vodRequest(ctx, http.MethodGet, q, nil, auth, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.ResponseMetadata.Error.Code != \"\" {\n\t\treturn nil, fmt.Errorf(\"wukong get upload candidates failed: %s\", resp.ResponseMetadata.Error.Message)\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Wukong) applyUploadInner(ctx context.Context, auth *uploadAuthTokenResp, uploadType string, fileSize int64, bestHosts string) (*applyUploadInnerResp, error) {\n\tspaceName := auth.SpaceName\n\tif uploadType == \"video\" {\n\t\tspaceName = videoSpaceName\n\t}\n\tq := map[string]string{\n\t\t\"Action\":          \"ApplyUploadInner\",\n\t\t\"Version\":         \"2020-11-19\",\n\t\t\"SpaceName\":       spaceName,\n\t\t\"FileType\":        uploadType,\n\t\t\"IsInner\":         \"1\",\n\t\t\"ClientBestHosts\": bestHosts,\n\t\t\"NeedFallback\":    \"true\",\n\t\t\"FileSize\":        strconv.FormatInt(fileSize, 10),\n\t\t\"s\":               randomString(8),\n\t}\n\tvar resp applyUploadInnerResp\n\tif err := d.vodRequest(ctx, http.MethodGet, q, nil, auth, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.ResponseMetadata.Error.Code != \"\" {\n\t\treturn nil, fmt.Errorf(\"wukong apply upload inner failed: %s\", resp.ResponseMetadata.Error.Message)\n\t}\n\treturn &resp, nil\n}\n\nfunc (d *Wukong) commitUploadInner(ctx context.Context, auth *uploadAuthTokenResp, spaceName, sessionKey string) (string, error) {\n\tq := map[string]string{\n\t\t\"Action\":    \"CommitUploadInner\",\n\t\t\"Version\":   \"2020-11-19\",\n\t\t\"SpaceName\": spaceName,\n\t}\n\tbody, _ := json.Marshal(map[string]any{\n\t\t\"SessionKey\": sessionKey,\n\t\t\"Functions\":  []any{},\n\t})\n\tvar resp commitUploadInnerResp\n\tif err := d.vodRequest(ctx, http.MethodPost, q, body, auth, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\tif resp.ResponseMetadata.Error.Code != \"\" {\n\t\treturn \"\", fmt.Errorf(\"wukong commit upload inner failed: %s\", resp.ResponseMetadata.Error.Message)\n\t}\n\tif len(resp.Result.Results) > 0 {\n\t\tstatus := resp.Result.Results[0].URIStatus\n\t\tif status != 0 && status != minUploadSubmitSuccess {\n\t\t\treturn \"\", fmt.Errorf(\"wukong commit upload inner failed: uri_status=%d\", status)\n\t\t}\n\t}\n\treturn extractVideoVid(&resp), nil\n}\n\nfunc (d *Wukong) uploadSubmit(ctx context.Context, fatherID, fileName, ext string, fileType int, size int64, md5Hex, storeURI, videoVid string) error {\n\tvar resp uploadSubmitResp\n\tbody := map[string]any{\n\t\t\"base_info\": map[string]any{\n\t\t\t\"father_id\":    asIDValue(fatherID),\n\t\t\t\"file_type\":    fileType,\n\t\t\t\"size\":         size,\n\t\t\t\"extension\":    ext,\n\t\t\t\"file_name\":    fileName,\n\t\t\t\"is_directory\": 0,\n\t\t\t\"md5\":          md5Hex,\n\t\t\t\"slice_md5\":    md5Hex,\n\t\t},\n\t}\n\tswitch fileType {\n\tcase 3000:\n\t\tif videoVid != \"\" {\n\t\t\tbody[\"video_info\"] = map[string]any{\"vid\": videoVid}\n\t\t}\n\tcase 2000:\n\t\tbody[\"image_info\"] = map[string]any{\"uri\": storeURI}\n\tdefault:\n\t\tbody[\"general_info\"] = map[string]any{\"key\": storeURI}\n\t}\n\t_, err := d.client.R().\n\t\tSetContext(ctx).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"aid\":             d.Aid,\n\t\t\t\"device_platform\": \"web\",\n\t\t\t\"language\":        d.Language,\n\t\t}).\n\t\tSetBody(body).\n\t\tSetResult(&resp).\n\t\tPost(\"/netdisk/upload_submit/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != 0 {\n\t\treturn fmt.Errorf(\"wukong upload submit failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc extractVideoVid(resp *commitUploadInnerResp) string {\n\tfor _, item := range resp.Result.Results {\n\t\tif item.Vid != \"\" {\n\t\t\treturn item.Vid\n\t\t}\n\t}\n\tif resp.Result.PluginResult != nil {\n\t\tif vid := findStringByKey(resp.Result.PluginResult, \"Vid\"); vid != \"\" {\n\t\t\treturn vid\n\t\t}\n\t\tif vid := findStringByKey(resp.Result.PluginResult, \"vid\"); vid != \"\" {\n\t\t\treturn vid\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc findStringByKey(v any, key string) string {\n\tswitch cur := v.(type) {\n\tcase map[string]any:\n\t\tif val, ok := cur[key]; ok {\n\t\t\tif s, ok := val.(string); ok && s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t\tfor _, child := range cur {\n\t\t\tif s := findStringByKey(child, key); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\tfor _, child := range cur {\n\t\t\tif s := findStringByKey(child, key); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (d *Wukong) uploadToTOS(ctx context.Context, host, storeURI, auth, storageUser, crc32Hex, fileName string, body io.Reader, size int64) error {\n\tvar resp tosUploadResp\n\tuploadURL := fmt.Sprintf(\"https://%s/upload/v1/%s\", host, storeURI)\n\treq := base.NewRestyClient().R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Host\", host).\n\t\tSetHeader(\"Referer\", webReferer).\n\t\tSetHeader(\"Origin\", \"https://pan.wkbrowser.com\").\n\t\tSetHeader(\"Authorization\", auth).\n\t\tSetHeader(\"Content-Type\", \"application/octet-stream\").\n\t\tSetHeader(\"Content-Crc32\", crc32Hex).\n\t\tSetHeader(\"Content-Disposition\", fmt.Sprintf(`attachment; filename=\"%s\"`, url.QueryEscape(fileName))).\n\t\tSetHeader(\"Content-Length\", strconv.FormatInt(size, 10)).\n\t\tSetBody(body).\n\t\tSetResult(&resp)\n\tif storageUser != \"\" {\n\t\treq.SetHeader(\"X-Storage-U\", storageUser)\n\t}\n\t_, err := req.Post(uploadURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != minUploadSubmitSuccess {\n\t\treturn fmt.Errorf(\"wukong upload to tos failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\tif resp.Data.Crc32 != \"\" && !strings.EqualFold(resp.Data.Crc32, crc32Hex) {\n\t\treturn fmt.Errorf(\"wukong upload to tos crc32 mismatch: local=%s remote=%s\", crc32Hex, resp.Data.Crc32)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) uploadToTOSMultipart(ctx context.Context, host, storeURI, auth, storageUser string, tempFile model.File, size int64, up driver.UpdateProgress) error {\n\tuploadID, err := d.initMultipartUpload(ctx, host, storeURI, auth, storageUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttotalParts := int((size + multipartChunkSize - 1) / multipartChunkSize)\n\tif totalParts <= 0 {\n\t\treturn errors.New(\"invalid multipart parts\")\n\t}\n\tparts := make([]string, 0, totalParts)\n\tfor i := 0; i < totalParts; i++ {\n\t\tpartNumber := i + 1\n\t\toffset := int64(i) * multipartChunkSize\n\t\tpartSize := multipartChunkSize\n\t\tif remain := size - offset; remain < partSize {\n\t\t\tpartSize = remain\n\t\t}\n\t\tbuf := make([]byte, partSize)\n\t\tn, readErr := tempFile.ReadAt(buf, offset)\n\t\tif readErr != nil && readErr != io.EOF {\n\t\t\treturn readErr\n\t\t}\n\t\tbuf = buf[:n]\n\t\tcrc32Hex := fmt.Sprintf(\"%08x\", crc32.ChecksumIEEE(buf))\n\t\tremoteCRC32, err := d.uploadMultipartPart(ctx, host, storeURI, auth, storageUser, uploadID, partNumber, buf, crc32Hex)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif remoteCRC32 != \"\" && !strings.EqualFold(remoteCRC32, crc32Hex) {\n\t\t\treturn fmt.Errorf(\"multipart part crc32 mismatch: part=%d local=%s remote=%s\", partNumber, crc32Hex, remoteCRC32)\n\t\t}\n\t\tparts = append(parts, fmt.Sprintf(\"%d:%s\", partNumber, crc32Hex))\n\t\tup(30 + float64(partNumber)/float64(totalParts)*50)\n\t}\n\n\treturn d.finishMultipartUpload(ctx, host, storeURI, auth, storageUser, uploadID, strings.Join(parts, \",\"))\n}\n\nfunc (d *Wukong) initMultipartUpload(ctx context.Context, host, storeURI, auth, storageUser string) (string, error) {\n\tvar resp tosUploadResp\n\treq := base.NewRestyClient().R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Host\", host).\n\t\tSetHeader(\"Referer\", webReferer).\n\t\tSetHeader(\"Origin\", \"https://pan.wkbrowser.com\").\n\t\tSetHeader(\"Authorization\", auth).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"uploadmode\": \"part\",\n\t\t\t\"phase\":      \"init\",\n\t\t}).\n\t\tSetResult(&resp)\n\tif storageUser != \"\" {\n\t\treq.SetHeader(\"X-Storage-U\", storageUser)\n\t}\n\tuploadURL := fmt.Sprintf(\"https://%s/upload/v1/%s\", host, storeURI)\n\t_, err := req.Post(uploadURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif resp.Code != minUploadSubmitSuccess {\n\t\treturn \"\", fmt.Errorf(\"wukong init multipart upload failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\tif resp.Data.UploadID == \"\" {\n\t\treturn \"\", errors.New(\"wukong init multipart upload returns empty uploadid\")\n\t}\n\treturn resp.Data.UploadID, nil\n}\n\nfunc (d *Wukong) uploadMultipartPart(ctx context.Context, host, storeURI, auth, storageUser, uploadID string, partNumber int, data []byte, crc32Hex string) (string, error) {\n\tvar resp tosUploadResp\n\treq := base.NewRestyClient().R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Host\", host).\n\t\tSetHeader(\"Referer\", webReferer).\n\t\tSetHeader(\"Origin\", \"https://pan.wkbrowser.com\").\n\t\tSetHeader(\"Authorization\", auth).\n\t\tSetHeader(\"Content-Type\", \"application/octet-stream\").\n\t\tSetHeader(\"Content-Crc32\", crc32Hex).\n\t\tSetHeader(\"Content-Length\", strconv.Itoa(len(data))).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"uploadid\":    uploadID,\n\t\t\t\"part_number\": strconv.Itoa(partNumber),\n\t\t\t\"phase\":       \"transfer\",\n\t\t}).\n\t\tSetBody(data).\n\t\tSetResult(&resp)\n\tif storageUser != \"\" {\n\t\treq.SetHeader(\"X-Storage-U\", storageUser)\n\t}\n\tuploadURL := fmt.Sprintf(\"https://%s/upload/v1/%s\", host, storeURI)\n\t_, err := req.Post(uploadURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif resp.Code != minUploadSubmitSuccess {\n\t\treturn \"\", fmt.Errorf(\"wukong multipart transfer failed: code=%d message=%s part=%d\", resp.Code, resp.Message, partNumber)\n\t}\n\treturn resp.Data.Crc32, nil\n}\n\nfunc (d *Wukong) finishMultipartUpload(ctx context.Context, host, storeURI, auth, storageUser, uploadID, body string) error {\n\tvar resp tosUploadResp\n\treq := base.NewRestyClient().R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Host\", host).\n\t\tSetHeader(\"Referer\", webReferer).\n\t\tSetHeader(\"Origin\", \"https://pan.wkbrowser.com\").\n\t\tSetHeader(\"Authorization\", auth).\n\t\tSetQueryParams(map[string]string{\n\t\t\t\"uploadid\":   uploadID,\n\t\t\t\"phase\":      \"finish\",\n\t\t\t\"uploadmode\": \"part\",\n\t\t}).\n\t\tSetBody(body).\n\t\tSetResult(&resp)\n\tif storageUser != \"\" {\n\t\treq.SetHeader(\"X-Storage-U\", storageUser)\n\t}\n\tuploadURL := fmt.Sprintf(\"https://%s/upload/v1/%s\", host, storeURI)\n\t_, err := req.Post(uploadURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.Code != minUploadSubmitSuccess && resp.Code != 4024 {\n\t\treturn fmt.Errorf(\"wukong multipart finish failed: code=%d message=%s\", resp.Code, resp.Message)\n\t}\n\treturn nil\n}\n\nfunc (d *Wukong) vodRequest(ctx context.Context, method string, query map[string]string, body []byte, auth *uploadAuthTokenResp, resp any) error {\n\treqURL := vodBaseURL + \"/\"\n\tamzDate := time.Now().UTC().Format(\"20060102T150405Z\")\n\tdateStamp := amzDate[:8]\n\theaders := map[string]string{\n\t\t\"x-amz-date\":           amzDate,\n\t\t\"x-amz-security-token\": auth.SessionToken,\n\t}\n\tif method == http.MethodPost {\n\t\theaders[\"x-amz-content-sha256\"] = hashSHA256Bytes(body)\n\t}\n\tauthorization := buildVodAuthorization(method, \"/\", query, headers, body, auth, dateStamp)\n\n\treq := base.NewRestyClient().R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Authorization\", authorization).\n\t\tSetHeader(\"x-amz-date\", amzDate).\n\t\tSetHeader(\"x-amz-security-token\", auth.SessionToken).\n\t\tSetQueryParams(query).\n\t\tSetResult(resp)\n\tif method == http.MethodPost {\n\t\treq.SetHeader(\"x-amz-content-sha256\", headers[\"x-amz-content-sha256\"])\n\t\treq.SetHeader(\"Content-Type\", \"text/plain;charset=UTF-8\")\n\t\treq.SetBody(body)\n\t}\n\t_, err := req.Execute(method, reqURL)\n\treturn err\n}\n\nfunc buildVodAuthorization(method, canonicalURI string, query map[string]string, headers map[string]string, body []byte, auth *uploadAuthTokenResp, dateStamp string) string {\n\tcanonicalQueryString := getCanonicalQueryStringFromMap(query)\n\tcanonicalHeaders, signedHeaders := getCanonicalHeaders(headers)\n\tpayloadHash := hashSHA256Bytes(body)\n\tcanonicalRequest := method + \"\\n\" + canonicalURI + \"\\n\" + canonicalQueryString + \"\\n\" + canonicalHeaders + \"\\n\" + signedHeaders + \"\\n\" + payloadHash\n\tcredentialScope := fmt.Sprintf(\"%s/%s/%s/aws4_request\", dateStamp, vodRegion, vodService)\n\tstringToSign := \"AWS4-HMAC-SHA256\\n\" + headers[\"x-amz-date\"] + \"\\n\" + credentialScope + \"\\n\" + hashSHA256String(canonicalRequest)\n\tsigningKey := getSigningKey(auth.SecretAccessKey, dateStamp, vodRegion, vodService)\n\tsignature := hmacSHA256Hex(signingKey, stringToSign)\n\treturn fmt.Sprintf(\"AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s\", auth.AccessKeyID, credentialScope, signedHeaders, signature)\n}\n\nfunc getCanonicalQueryStringFromMap(query map[string]string) string {\n\tif len(query) == 0 {\n\t\treturn \"\"\n\t}\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\tparts := make([]string, 0, len(keys))\n\tfor _, k := range keys {\n\t\tparts = append(parts, awsURLEncode(k)+\"=\"+awsURLEncode(query[k]))\n\t}\n\treturn strings.Join(parts, \"&\")\n}\n\nfunc getCanonicalHeaders(headers map[string]string) (string, string) {\n\tkeys := make([]string, 0, len(headers))\n\tfor k := range headers {\n\t\tkeys = append(keys, strings.ToLower(k))\n\t}\n\tsort.Strings(keys)\n\tvar h strings.Builder\n\tfor _, k := range keys {\n\t\th.WriteString(k)\n\t\th.WriteString(\":\")\n\t\th.WriteString(strings.TrimSpace(headers[k]))\n\t\th.WriteString(\"\\n\")\n\t}\n\treturn h.String(), strings.Join(keys, \";\")\n}\n\nfunc awsURLEncode(s string) string {\n\ts = url.QueryEscape(s)\n\treturn strings.ReplaceAll(s, \"+\", \"%20\")\n}\n\nfunc hashSHA256Bytes(data []byte) string {\n\tsum := sha256.Sum256(data)\n\treturn hex.EncodeToString(sum[:])\n}\n\nfunc hashSHA256String(s string) string {\n\treturn hashSHA256Bytes([]byte(s))\n}\n\nfunc hmacSHA256(key []byte, data string) []byte {\n\th := hmac.New(sha256.New, key)\n\t_, _ = h.Write([]byte(data))\n\treturn h.Sum(nil)\n}\n\nfunc hmacSHA256Hex(key []byte, data string) string {\n\treturn hex.EncodeToString(hmacSHA256(key, data))\n}\n\nfunc getSigningKey(secret, dateStamp, region, service string) []byte {\n\tkDate := hmacSHA256([]byte(\"AWS4\"+secret), dateStamp)\n\tkRegion := hmacSHA256(kDate, region)\n\tkService := hmacSHA256(kRegion, service)\n\treturn hmacSHA256(kService, \"aws4_request\")\n}\n\nfunc collectCandidateHosts(resp *getUploadCandidatesResp) []string {\n\tseen := map[string]struct{}{}\n\thosts := make([]string, 0, len(resp.Result.Domains))\n\tadd := func(domain vodDomain) {\n\t\tif domain.Name == \"\" {\n\t\t\treturn\n\t\t}\n\t\tif _, ok := seen[domain.Name]; ok {\n\t\t\treturn\n\t\t}\n\t\tseen[domain.Name] = struct{}{}\n\t\thosts = append(hosts, domain.Name)\n\t}\n\tfor _, candidate := range resp.Result.Candidates {\n\t\tfor _, domain := range candidate.Domains {\n\t\t\tadd(domain)\n\t\t}\n\t}\n\tfor _, domain := range resp.Result.Domains {\n\t\tadd(domain)\n\t}\n\treturn hosts\n}\n\nfunc getStorageUserID(header map[string]any) string {\n\tif header == nil {\n\t\treturn \"\"\n\t}\n\tif s, ok := header[\"USER_ID\"].(string); ok {\n\t\treturn s\n\t}\n\tif f, ok := header[\"USER_ID\"].(float64); ok {\n\t\treturn strconv.FormatInt(int64(f), 10)\n\t}\n\treturn \"\"\n}\n\nfunc calcFileMD5AndCRC32(f model.File) (string, string, error) {\n\tif _, err := f.Seek(0, io.SeekStart); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tmd5Hasher := md5.New()\n\tcrc := crc32.NewIEEE()\n\t_, err := io.Copy(io.MultiWriter(md5Hasher, crc), f)\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\treturn hex.EncodeToString(md5Hasher.Sum(nil)), fmt.Sprintf(\"%08x\", crc.Sum32()), nil\n}\n\nfunc detectWukongFileType(mimetype, fileName string) int {\n\tlowerName := strings.ToLower(fileName)\n\tswitch {\n\tcase strings.HasPrefix(mimetype, \"image/\"):\n\t\treturn 2000\n\tcase strings.HasPrefix(mimetype, \"video/\"), strings.HasSuffix(lowerName, \".flv\"), strings.HasSuffix(lowerName, \".mkv\"):\n\t\treturn 3000\n\tcase strings.HasPrefix(mimetype, \"audio/\"), strings.HasSuffix(lowerName, \".mp3\"), strings.HasSuffix(lowerName, \".m4a\"), strings.HasSuffix(lowerName, \".wav\"):\n\t\treturn 4000\n\tcase strings.HasSuffix(lowerName, \".zip\"), strings.HasSuffix(lowerName, \".rar\"), strings.HasSuffix(lowerName, \".7z\"), strings.HasSuffix(lowerName, \".tar\"), strings.HasSuffix(lowerName, \".gz\"), strings.HasSuffix(lowerName, \".tgz\"):\n\t\treturn 6000\n\tdefault:\n\t\treturn 5000\n\t}\n}\n\nfunc randomString(n int) string {\n\tconst letters = \"abcdefghijklmnopqrstuvwxyz0123456789\"\n\tif n <= 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, n)\n\tif _, err := crand.Read(buf); err == nil {\n\t\tfor i := range buf {\n\t\t\tbuf[i] = letters[int(buf[i])%len(letters)]\n\t\t}\n\t\treturn string(buf)\n\t}\n\n\tnow := uint64(time.Now().UnixNano())\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tnow = now*6364136223846793005 + 1\n\t\tb[i] = letters[int(now%uint64(len(letters)))]\n\t}\n\treturn string(b)\n}\n\nfunc uploadSourceByType(uploadType string) string {\n\tswitch uploadType {\n\tcase \"video\":\n\t\treturn \"10150001\"\n\tcase \"image\":\n\t\treturn \"20150001\"\n\tdefault:\n\t\treturn \"50150001\"\n\t}\n}\n\nfunc detectUploadType(mimetype, fileName string) string {\n\tlowerName := strings.ToLower(fileName)\n\tif strings.HasPrefix(mimetype, \"video/\") || strings.HasPrefix(mimetype, \"audio/\") ||\n\t\tstrings.HasSuffix(lowerName, \".flv\") || strings.HasSuffix(lowerName, \".mkv\") ||\n\t\tstrings.HasSuffix(lowerName, \".mp3\") || strings.HasSuffix(lowerName, \".m4a\") || strings.HasSuffix(lowerName, \".wav\") {\n\t\treturn \"video\"\n\t}\n\tif strings.HasPrefix(mimetype, \"image/\") {\n\t\treturn \"image\"\n\t}\n\treturn \"object\"\n}\n\nfunc chooseCommitSpace(uploadType, authSpace string) string {\n\tif uploadType == \"video\" {\n\t\treturn videoSpaceName\n\t}\n\treturn authSpace\n}\n\nfunc asIDValue(id string) any {\n\tif n, err := strconv.ParseInt(id, 10, 64); err == nil {\n\t\treturn n\n\t}\n\treturn id\n}\n\nfunc parseUnix(ts int64) time.Time {\n\tif ts <= 0 {\n\t\treturn time.Time{}\n\t}\n\tif ts > 1e12 {\n\t\treturn time.UnixMilli(ts)\n\t}\n\treturn time.Unix(ts, 0)\n}\n\nfunc hasMore(v any) bool {\n\tswitch val := v.(type) {\n\tcase bool:\n\t\treturn val\n\tcase float64:\n\t\treturn val != 0\n\tcase int:\n\t\treturn val != 0\n\tcase int64:\n\t\treturn val != 0\n\tcase string:\n\t\treturn val == \"1\" || strings.EqualFold(val, \"true\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc extractURL(data map[string]any) string {\n\tpriority := []string{\n\t\t\"download_url\",\n\t\t\"main_url\",\n\t\t\"MainUrl\",\n\t\t\"MainHTTPUrl\",\n\t\t\"url\",\n\t\t\"source_url\",\n\t\t\"play_url\",\n\t\t\"backup_url\",\n\t\t\"BackupUrl\",\n\t\t\"BackupHTTPUrl\",\n\t}\n\tfor _, key := range priority {\n\t\tif url := findURLByKey(data, key); url != \"\" {\n\t\t\treturn url\n\t\t}\n\t}\n\treturn findAnyHTTPURL(data)\n}\n\nfunc extractDetailMainURL(data map[string]any) string {\n\trawList, ok := data[\"list\"]\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tlist, ok := rawList.([]any)\n\tif !ok || len(list) == 0 {\n\t\treturn \"\"\n\t}\n\tfirst, ok := list[0].(map[string]any)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tgeneralInfo, ok := first[\"general_info\"].(map[string]any)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tmainURL, ok := generalInfo[\"main_url\"].(string)\n\tif !ok || !isHTTPURL(mainURL) {\n\t\treturn \"\"\n\t}\n\treturn mainURL\n}\n\nfunc findURLByKey(v any, key string) string {\n\tswitch cur := v.(type) {\n\tcase map[string]any:\n\t\tif val, ok := cur[key]; ok {\n\t\t\tif s, ok := val.(string); ok && isHTTPURL(s) {\n\t\t\t\treturn s\n\t\t\t}\n\t\t\tif decoded := tryDecodeJSONAny(val); decoded != nil {\n\t\t\t\tif s := findURLByKey(decoded, key); s != \"\" {\n\t\t\t\t\treturn s\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, child := range cur {\n\t\t\tif s := findURLByKey(child, key); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\tfor _, child := range cur {\n\t\t\tif s := findURLByKey(child, key); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\tcase string:\n\t\tif decoded := tryDecodeJSONString(cur); decoded != nil {\n\t\t\treturn findURLByKey(decoded, key)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc findAnyHTTPURL(v any) string {\n\tswitch cur := v.(type) {\n\tcase string:\n\t\tif isHTTPURL(cur) {\n\t\t\treturn cur\n\t\t}\n\t\tif decoded := tryDecodeJSONString(cur); decoded != nil {\n\t\t\treturn findAnyHTTPURL(decoded)\n\t\t}\n\tcase map[string]any:\n\t\tfor _, child := range cur {\n\t\t\tif s := findAnyHTTPURL(child); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\tfor _, child := range cur {\n\t\t\tif s := findAnyHTTPURL(child); s != \"\" {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc isHTTPURL(v string) bool {\n\treturn strings.HasPrefix(v, \"http://\") || strings.HasPrefix(v, \"https://\")\n}\n\nfunc tryDecodeJSONAny(v any) any {\n\ts, ok := v.(string)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn tryDecodeJSONString(s)\n}\n\nfunc tryDecodeJSONString(s string) any {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\tif !(strings.HasPrefix(s, \"{\") || strings.HasPrefix(s, \"[\")) {\n\t\treturn nil\n\t}\n\tvar out any\n\tif err := json.Unmarshal([]byte(s), &out); err != nil {\n\t\treturn nil\n\t}\n\treturn out\n}\n\nvar _ driver.Driver = (*Wukong)(nil)\n"
  },
  {
    "path": "drivers/wukong/meta.go",
    "content": "package wukong\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n)\n\ntype Addition struct {\n\tdriver.RootID\n\tCookie   string `json:\"cookie\" type:\"text\" required:\"true\" help:\"Cookie from https://pan.wkbrowser.com/\"`\n\tAid      string `json:\"aid\" default:\"590353\" help:\"aid query param used by web requests\"`\n\tLanguage string `json:\"language\" default:\"zh\"`\n\tPageSize int    `json:\"page_size\" type:\"number\" default:\"100\"`\n}\n\nvar config = driver.Config{\n\tName:              \"WuKongNetdisk\",\n\tLocalSort:         false,\n\tOnlyLocal:         false,\n\tOnlyProxy:         false,\n\tNoCache:           false,\n\tNoUpload:          false,\n\tNeedMs:            false,\n\tDefaultRoot:       \"0\",\n\tCheckStatus:       false,\n\tAlert:             \"\",\n\tNoOverwriteUpload: true,\n}\n\nfunc init() {\n\top.RegisterDriver(func() driver.Driver {\n\t\treturn &Wukong{}\n\t})\n}\n"
  },
  {
    "path": "drivers/wukong/types.go",
    "content": "package wukong\n\ntype filterFileResp struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tFileList []wukongFile `json:\"file_list\"`\n\t\tHasMore  any          `json:\"has_more\"`\n\t} `json:\"data\"`\n}\n\ntype wukongFile struct {\n\tFileID      int64  `json:\"file_id\"`\n\tFatherID    int64  `json:\"father_id\"`\n\tIsDirectory int    `json:\"is_directory\"`\n\tFileType    int    `json:\"file_type\"`\n\tSize        int64  `json:\"size\"`\n\tFileName    string `json:\"file_name\"`\n\tCreatedAt   int64  `json:\"created_at\"`\n\tUpdatedAt   int64  `json:\"updated_at\"`\n}\n\ntype rawResp struct {\n\tCode    int            `json:\"code\"`\n\tMessage string         `json:\"message\"`\n\tData    map[string]any `json:\"data\"`\n}\n\ntype uploadAuthTokenResp struct {\n\tCode            int    `json:\"code\"`\n\tMessage         string `json:\"message\"`\n\tCurrentTime     int64  `json:\"current_time\"`\n\tExpireTime      int64  `json:\"expire_time\"`\n\tSpaceName       string `json:\"space_name\"`\n\tAccessKeyID     string `json:\"access_key_id\"`\n\tSecretAccessKey string `json:\"secret_access_key\"`\n\tSessionToken    string `json:\"session_token\"`\n}\n\ntype vodResponseMetadata 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 vodDomain struct {\n\tName    string `json:\"Name\"`\n\tSign    string `json:\"Sign\"`\n\tStoreID string `json:\"StoreID\"`\n}\n\ntype getUploadCandidatesResp struct {\n\tResponseMetadata vodResponseMetadata `json:\"ResponseMetadata\"`\n\tResult           struct {\n\t\tCandidates []struct {\n\t\t\tDomains []vodDomain `json:\"Domains\"`\n\t\t} `json:\"Candidates\"`\n\t\tDomains []vodDomain `json:\"Domains\"`\n\t} `json:\"Result\"`\n}\n\ntype applyUploadInnerResp struct {\n\tResponseMetadata vodResponseMetadata `json:\"ResponseMetadata\"`\n\tResult           struct {\n\t\tInnerUploadAddress struct {\n\t\t\tUploadNodes []struct {\n\t\t\t\tStoreInfos []struct {\n\t\t\t\t\tStoreURI      string         `json:\"StoreUri\"`\n\t\t\t\t\tAuth          string         `json:\"Auth\"`\n\t\t\t\t\tUploadID      string         `json:\"UploadID\"`\n\t\t\t\t\tStorageHeader map[string]any `json:\"StorageHeader\"`\n\t\t\t\t} `json:\"StoreInfos\"`\n\t\t\t\tUploadHost string `json:\"UploadHost\"`\n\t\t\t\tSessionKey string `json:\"SessionKey\"`\n\t\t\t} `json:\"UploadNodes\"`\n\t\t} `json:\"InnerUploadAddress\"`\n\t} `json:\"Result\"`\n}\n\ntype tosUploadResp struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n\tData    struct {\n\t\tCrc32      string `json:\"crc32\"`\n\t\tUploadID   string `json:\"uploadid\"`\n\t\tPartNumber string `json:\"part_number\"`\n\t\tEtag       string `json:\"etag\"`\n\t} `json:\"data\"`\n}\n\ntype commitUploadInnerResp struct {\n\tResponseMetadata vodResponseMetadata `json:\"ResponseMetadata\"`\n\tResult           struct {\n\t\tResults []struct {\n\t\t\tURI       string `json:\"Uri\"`\n\t\t\tURIStatus int    `json:\"UriStatus\"`\n\t\t\tVid       string `json:\"Vid\"`\n\t\t} `json:\"Results\"`\n\t\tPluginResult any `json:\"PluginResult\"`\n\t} `json:\"Result\"`\n}\n\ntype uploadSubmitResp struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\treturn fileToObj(src), 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/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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\tClientID     string `json:\"client_id\" required:\"true\" default:\"a78d5a69054042fa936f6c77f9a0ae8b\"`\n\tClientSecret string `json:\"client_secret\" required:\"true\" default:\"9c119bbb04b346d2a52aa64401936b2b\"`\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/alist-org/alist/v3/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.Obj {\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/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/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\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  ./alist version\nelse\n  if [ \"$RUN_ARIA2\" = \"true\" ]; then\n    chown -R ${PUID}:${PGID} /opt/aria2/\n    exec su-exec ${PUID}:${PGID} nohup aria2c \\\n      --enable-rpc \\\n      --rpc-allow-origin-all \\\n      --conf-path=/opt/aria2/.aria2/aria2.conf \\\n      >/dev/null 2>&1 &\n  fi\n\n  chown -R ${PUID}:${PGID} /opt/alist/\n  exec su-exec ${PUID}:${PGID} ./alist server --no-prefix\nfi"
  },
  {
    "path": "go.mod",
    "content": "module github.com/alist-org/alist/v3\n\ngo 1.23.4\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0\n\tgithub.com/KirCute/ftpserverlib-pasvportmap v1.25.0\n\tgithub.com/KirCute/sftpd-alist v0.0.12\n\tgithub.com/ProtonMail/go-crypto v1.0.0\n\tgithub.com/ProtonMail/gopenpgp/v2 v2.7.4\n\tgithub.com/SheltonZhu/115driver v1.2.3-1\n\tgithub.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21\n\tgithub.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4\n\tgithub.com/alist-org/gofakes3 v0.0.7\n\tgithub.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92\n\tgithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible\n\tgithub.com/avast/retry-go v3.0.0+incompatible\n\tgithub.com/aws/aws-sdk-go v1.55.5\n\tgithub.com/blevesearch/bleve/v2 v2.4.2\n\tgithub.com/caarlos0/env/v9 v9.0.0\n\tgithub.com/charmbracelet/bubbles v0.20.0\n\tgithub.com/charmbracelet/bubbletea v1.1.0\n\tgithub.com/charmbracelet/lipgloss v0.13.0\n\tgithub.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e\n\tgithub.com/coreos/go-oidc v2.2.1+incompatible\n\tgithub.com/deckarep/golang-set/v2 v2.6.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.4\n\tgithub.com/dustinxie/ecc v0.0.0-20210511000915-959544187564\n\tgithub.com/foxxorcat/mopan-sdk-go v0.1.6\n\tgithub.com/foxxorcat/weiyun-sdk-go v0.1.3\n\tgithub.com/gin-contrib/cors v1.7.2\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/go-resty/resty/v2 v2.14.0\n\tgithub.com/go-webauthn/webauthn v0.11.1\n\tgithub.com/golang-jwt/jwt/v4 v4.5.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/hekmon/transmissionrpc/v3 v3.0.0\n\tgithub.com/henrybear327/Proton-API-Bridge v1.0.0\n\tgithub.com/henrybear327/go-proton-api v1.0.0\n\tgithub.com/hirochachacha/go-smb2 v1.1.0\n\tgithub.com/ipfs/go-ipfs-api v0.7.0\n\tgithub.com/jlaffaye/ftp v0.2.0\n\tgithub.com/json-iterator/go v1.1.12\n\tgithub.com/kdomanski/iso9660 v0.4.0\n\tgithub.com/larksuite/oapi-sdk-go/v3 v3.3.1\n\tgithub.com/maruel/natural v1.1.1\n\tgithub.com/meilisearch/meilisearch-go v0.27.2\n\tgithub.com/mholt/archives v0.1.0\n\tgithub.com/minio/sio v0.4.0\n\tgithub.com/natefinch/lumberjack v2.0.0+incompatible\n\tgithub.com/ncw/swift/v2 v2.0.3\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/pkg/sftp v1.13.6\n\tgithub.com/pquerna/otp v1.4.0\n\tgithub.com/rclone/rclone v1.67.0\n\tgithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/afero v1.11.0\n\tgithub.com/spf13/cobra v1.8.1\n\tgithub.com/stretchr/testify v1.10.0\n\tgithub.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7\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.5.1-0.20230130140708-f87f5db493b5\n\tgithub.com/xhofe/tache v0.1.5\n\tgithub.com/xhofe/wopan-sdk-go v0.1.3\n\tgithub.com/yeka/zip v0.0.0-20231116150916-03d6312748a9\n\tgithub.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22\n\tgolang.org/x/crypto v0.36.0\n\tgolang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e\n\tgolang.org/x/image v0.19.0\n\tgolang.org/x/net v0.38.0\n\tgolang.org/x/oauth2 v0.30.0\n\tgolang.org/x/time v0.8.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\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // 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.8.1 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.2 // indirect\n\tgithub.com/bradenaw/juniper v0.15.2 // indirect\n\tgithub.com/cronokirby/saferith v0.33.0 // indirect\n\tgithub.com/emersion/go-message v0.18.0 // indirect\n\tgithub.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect\n\tgithub.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect\n\tgithub.com/relvacode/iso8601 v1.3.0 // indirect\n)\n\nrequire (\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.20 // indirect\n\tgithub.com/blevesearch/zapx/v16 v16.1.5 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.6.0\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/bytedance/sonic/loader v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.2.3 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.0 // indirect\n\tgithub.com/cloudflare/circl v1.3.7 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // 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.5.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.0.0-beta.4.0.20241112120701-034e449c6e78\n\tgithub.com/sorairolake/lzip-go v0.3.5 // indirect\n\tgithub.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 // indirect\n\tgithub.com/therootcompany/xz v1.0.1 // indirect\n\tgithub.com/ulikunitz/xz v0.5.12 // indirect\n\tgithub.com/xhofe/115-sdk-go v0.1.5\n\tgithub.com/yuin/goldmark v1.7.8\n\tgo4.org v0.0.0-20230225012048-214862532bf5\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/RoaringBitmap/roaring v1.9.3 // 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.1 // 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.12.0 // indirect\n\tgithub.com/blang/semver/v4 v4.0.0 // indirect\n\tgithub.com/blevesearch/bleve_index_api v1.1.10 // indirect\n\tgithub.com/blevesearch/geo v0.1.20 // 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.2.15 // 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.0.10 // indirect\n\tgithub.com/blevesearch/zapx/v11 v11.3.10 // indirect\n\tgithub.com/blevesearch/zapx/v12 v12.3.10 // indirect\n\tgithub.com/blevesearch/zapx/v13 v13.3.10 // indirect\n\tgithub.com/blevesearch/zapx/v14 v14.3.10 // indirect\n\tgithub.com/blevesearch/zapx/v15 v15.3.13 // indirect\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/bytedance/sonic v1.11.6 // 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.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/geoffgarside/ber v1.1.0 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.0.12 // 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.20.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.7.0 // indirect\n\tgithub.com/go-webauthn/x v0.1.12 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.2.1 // indirect\n\tgithub.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // 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.1 // 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/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/ipfs/go-cid v0.4.1\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.17.11 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // 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-20231016141302-07b5767bb0ed // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // 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.15.2 // 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/otiai10/copy v1.14.0\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.21 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect\n\tgithub.com/pquerna/cachecontrol v0.1.0 // indirect\n\tgithub.com/prometheus/client_golang v1.19.1 // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.48.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // 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/shirou/gopsutil/v3 v3.24.4 // indirect\n\tgithub.com/shoenig/go-m1cpu v0.2.0 // indirect\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect\n\tgithub.com/spaolacci/murmur3 v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.13 // indirect\n\tgithub.com/tklauser/numcpus v0.7.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.2.12 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.etcd.io/bbolt v1.3.8 // indirect\n\tgolang.org/x/arch v0.8.0 // indirect\n\tgolang.org/x/sync v0.12.0\n\tgolang.org/x/sys v0.31.0 // indirect\n\tgolang.org/x/term v0.30.0 // indirect\n\tgolang.org/x/text v0.23.0\n\tgolang.org/x/tools v0.24.0 // indirect\n\tgoogle.golang.org/api v0.169.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect\n\tgoogle.golang.org/grpc v1.66.0\n\tgoogle.golang.org/protobuf v1.34.2 // 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/square/go-jose.v2 v2.6.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\nreplace github.com/SheltonZhu/115driver => github.com/okatu-loli/115driver v1.2.3-1\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/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=\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 v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw=\ncloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=\ncloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=\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.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.3.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/KirCute/ftpserverlib-pasvportmap v1.25.0 h1:ikwCzeqoqN6wvBHOB9OI6dde/jbV7EoTMpUcxtYl5Po=\ngithub.com/KirCute/ftpserverlib-pasvportmap v1.25.0/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=\ngithub.com/KirCute/sftpd-alist v0.0.12 h1:GNVM5QLbQLAfXP4wGUlXFA2IO6fVek0n0IsGnOuISdg=\ngithub.com/KirCute/sftpd-alist v0.0.12/go.mod h1:2wNK7yyW2XfjyJq10OY6xB4COLac64hOwfV6clDJn6s=\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/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 v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=\ngithub.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=\ngithub.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=\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.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF21O1mRlo=\ngithub.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=\ngithub.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=\ngithub.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=\ngithub.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=\ngithub.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=\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/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=\ngithub.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=\ngithub.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21 h1:h6q5E9aMBhhdqouW81LozVPI1I+Pu6IxL2EKpfm5OjY=\ngithub.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=\ngithub.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4 h1:WnvifFgYyogPz2ZFvaVLk4gI/Co0paF92FmxSR6U1zY=\ngithub.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4/go.mod h1:8pWlL2rpusvx7Xa6yYaIWOJ8bR3gPdFBUT7OystyGOY=\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/alist-org/gofakes3 v0.0.7 h1:0cDGI7fLBrqumhCBto9T3ZYCL71AyGZ1l+xxJgjqe8s=\ngithub.com/alist-org/gofakes3 v0.0.7/go.mod h1:6IyGtYGIX29fLvtXo+XZhtwX2P33KVYYj8uTgAHSu58=\ngithub.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92 h1:pIEI87zhv8ZzQcu65rTL7kqirrs8dR6HDiXrqWat2Fk=\ngithub.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92/go.mod h1:oPJwGY3sLmGgcJamGumz//0A35f4BwQRacyqLNcJTOU=\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.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=\ngithub.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=\ngithub.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=\ngithub.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=\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.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=\ngithub.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=\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 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=\ngithub.com/bits-and-blooms/bitset v1.12.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.4.2 h1:NooYP1mb3c0StkiY9/xviiq2LGSaE8BQBCc/pirMx0U=\ngithub.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8=\ngithub.com/blevesearch/bleve_index_api v1.1.10 h1:PDLFhVjrjQWr6jCuU7TwlmByQVCSEURADHdCqVS9+g0=\ngithub.com/blevesearch/bleve_index_api v1.1.10/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=\ngithub.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=\ngithub.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=\ngithub.com/blevesearch/go-faiss v1.0.20 h1:AIkdTQFWuZ5LQmKQSebgMR4RynGNw8ZseJXaan5kvtI=\ngithub.com/blevesearch/go-faiss v1.0.20/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=\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.2.15 h1:prV17iU/o+A8FiZi9MXmqbagd8I0bCqM7OKUYPbnb5Y=\ngithub.com/blevesearch/scorch_segment_api/v2 v2.2.15/go.mod h1:db0cmP03bPNadXrCDuVkKLV6ywFSiRgPFT1YVrestBc=\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.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=\ngithub.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=\ngithub.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=\ngithub.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=\ngithub.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=\ngithub.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=\ngithub.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=\ngithub.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=\ngithub.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=\ngithub.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=\ngithub.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=\ngithub.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=\ngithub.com/blevesearch/zapx/v16 v16.1.5 h1:b0sMcarqNFxuXvjoXsF8WtwVahnxyhEvBSRJi/AUHjU=\ngithub.com/blevesearch/zapx/v16 v16.1.5/go.mod h1:J4mSF39w1QELc11EWRSBFkPeZuO7r/NPKkHzDCoiaI8=\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.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=\ngithub.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=\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.2 h1:0JdjBGEF2jP1pOxmlNIrPhAoQN7Ng5IMAY5D0PHMW4U=\ngithub.com/bradenaw/juniper v0.15.2/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8=\ngithub.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\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.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=\ngithub.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=\ngithub.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=\ngithub.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=\ngithub.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=\ngithub.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=\ngithub.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=\ngithub.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=\ngithub.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=\ngithub.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=\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.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=\ngithub.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=\ngithub.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=\ngithub.com/coreos/go-oidc v2.2.1+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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\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.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=\ngithub.com/deckarep/golang-set/v2 v2.6.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.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=\ngithub.com/dlclark/regexp2 v1.11.4/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/emersion/go-message v0.18.0 h1:7LxAXHRpSeoO/Wom3ZApVZYG7c3d17yCScYce8WiXA8=\ngithub.com/emersion/go-message v0.18.0/go.mod h1:Zi69ACvzaoV/MBnrxfVBPV3xWEuCmC2nEN39oJF4B8A=\ngithub.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=\ngithub.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=\ngithub.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=\ngithub.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/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/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=\ngithub.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=\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.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=\ngithub.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=\ngithub.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=\ngithub.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=\ngithub.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=\ngithub.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=\ngithub.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=\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.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=\ngithub.com/go-logfmt/logfmt v0.5.1/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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=\ngithub.com/go-logr/logr v1.4.1/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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU=\ngithub.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg=\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.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA=\ngithub.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE=\ngithub.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A=\ngithub.com/go-webauthn/x v0.1.12/go.mod h1:XlRcGkNH8PT45TfeJYc6gqpOtiOendHhVmnOxh+5yHs=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=\ngithub.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=\ngithub.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=\ngithub.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=\ngithub.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=\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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=\ngithub.com/google/go-tpm v0.9.1/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.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=\ngithub.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=\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.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=\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.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=\ngithub.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=\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/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\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/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-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/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=\ngithub.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=\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.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=\ngithub.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=\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/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/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.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=\ngithub.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=\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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\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.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=\ngithub.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=\ngithub.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=\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.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\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/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=\ngithub.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=\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-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0=\ngithub.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\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.27.2 h1:3G21dJ5i208shnLPDsIEZ0L0Geg/5oeXABFV7nlK94k=\ngithub.com/meilisearch/meilisearch-go v0.27.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=\ngithub.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=\ngithub.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=\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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=\ngithub.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=\ngithub.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=\ngithub.com/minio/sio v0.4.0/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I=\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.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=\ngithub.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=\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/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.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=\ngithub.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=\ngithub.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY=\ngithub.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=\ngithub.com/okatu-loli/115driver v1.2.3-1 h1:UoBEREqh6RD6WlxiJ2Z29JxNZ/UcoChvdHn9r9Tx7nI=\ngithub.com/okatu-loli/115driver v1.2.3-1/go.mod h1:Zk7Qz7SYO1QU0SJIne6DnUD2k36S3wx/KbsQpxcfY/Y=\ngithub.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=\ngithub.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=\ngithub.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=\ngithub.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=\ngithub.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=\ngithub.com/pierrec/lz4/v4 v4.1.21/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.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=\ngithub.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=\ngithub.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=\ngithub.com/pkg/xattr v0.4.9/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-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=\ngithub.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/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.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=\ngithub.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=\ngithub.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=\ngithub.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/rclone/rclone v1.67.0 h1:yLRNgHEG2vQ60HCuzFqd0hYwKCRuWuvPUhvhMJ2jI5E=\ngithub.com/rclone/rclone v1.67.0/go.mod h1:Cb3Ar47M/SvwfhAjZTbVXdtrP/JLtPFCq2tkdtBVC6w=\ngithub.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko=\ngithub.com/relvacode/iso8601 v1.3.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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\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/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=\ngithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=\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/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=\ngithub.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=\ngithub.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=\ngithub.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=\ngithub.com/shoenig/go-m1cpu v0.2.0 h1:t4GNqvPZ84Vjtpboo/kT3pIkbaK3vc+JIlD/Wz1zSFY=\ngithub.com/shoenig/go-m1cpu v0.2.0/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=\ngithub.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=\ngithub.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=\ngithub.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=\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.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=\ngithub.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/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/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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\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-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=\ngithub.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=\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/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=\ngithub.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=\ngithub.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=\ngithub.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=\ngithub.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=\ngithub.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=\ngithub.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=\ngithub.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=\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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\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/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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d h1:xS9QTPgKl9ewGsAOPc+xW7DeStJDqYPfisDmeSCcbco=\ngithub.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=\ngithub.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=\ngithub.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5 h1:jxZvjx8Ve5sOXorZG0KzTxbp0Cr1n3FEegfmyd9br1k=\ngithub.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5/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/xhofe/115-sdk-go v0.1.5 h1:2+E92l6AX0+ABAkrdmDa9PE5ONN7wVLCaKkK80zETOg=\ngithub.com/xhofe/115-sdk-go v0.1.5/go.mod h1:MIdpe/4Kw4ODrPld7E11bANc4JsCuXcm5ZZBHSiOI0U=\ngithub.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3KP7BT2dot2CvJGIvrB0NEoDXI=\ngithub.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=\ngithub.com/xhofe/tache v0.1.5 h1:ezDcgim7tj7KNMXliQsmf8BJQbaZtitfyQA9Nt+B4WM=\ngithub.com/xhofe/tache v0.1.5/go.mod h1:PYt6I/XUKliSg1uHlgsk6ha+le/f6PAvjUtFZAVl3a8=\ngithub.com/xhofe/wopan-sdk-go v0.1.3 h1:J58X6v+n25ewBZjb05pKOr7AWGohb+Rdll4CThGh6+A=\ngithub.com/xhofe/wopan-sdk-go v0.1.3/go.mod h1:dcY9yA28fnaoZPnXZiVTFSkcd7GnIPTpTIIlfSI5z5Q=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yeka/zip v0.0.0-20231116150916-03d6312748a9 h1:K8gF0eekWPEX+57l30ixxzGhHH/qscI3JCnuhbN6V4M=\ngithub.com/yeka/zip v0.0.0-20231116150916-03d6312748a9/go.mod h1:9BnoKCcgJ/+SLhfAXj15352hTOuVmG5Gzo8xNRINfqI=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=\ngithub.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\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-20221216044934-fd1c571e3a22 h1:X+lHsNTlbatQ1cErXIbtyrh+3MTWxqQFS+sBP/wpFXo=\ngithub.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22/go.mod h1:1zGRDJd8zlG6P8azG96+uywfh6udYWwhOmUivw+xsuM=\ngo.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=\ngo.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=\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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=\ngo.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=\ngo.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=\ngo.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=\ngo.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=\ngo.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=\ngo.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=\ngo.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=\ngo.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=\ngolang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\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-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=\ngolang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=\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.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=\ngolang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=\ngolang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=\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-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=\ngolang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=\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.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=\ngolang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=\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.3.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/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\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-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\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.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=\ngolang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=\ngolang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=\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/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-20201020160332-67f06af15bc9/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=\ngolang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\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-20210423082822-04245dca01da/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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.3.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.11.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.19.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.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=\ngolang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\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.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=\ngolang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=\ngolang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=\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.6/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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=\ngolang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=\ngolang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=\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.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=\ngolang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=\ngolang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=\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=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY=\ngoogle.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=\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-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=\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.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=\ngoogle.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=\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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\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/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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=\ngopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\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.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/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\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/alist-org/alist/v3/internal/archive/archives\"\n\t_ \"github.com/alist-org/alist/v3/internal/archive/iso9660\"\n\t_ \"github.com/alist-org/alist/v3/internal/archive/rardecode\"\n\t_ \"github.com/alist-org/alist/v3/internal/archive/sevenzip\"\n\t_ \"github.com/alist-org/alist/v3/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\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype Archives struct {\n}\n\nfunc (Archives) AcceptedExtensions() []string {\n\treturn []string{\n\t\t\".br\", \".bz2\", \".gz\", \".lz4\", \".lz\", \".sz\", \".s2\", \".xz\", \".zz\", \".zst\", \".tar\",\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\tif p == path {\n\t\t\t\tif d.IsDir() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\trelPath := strings.TrimPrefix(p, path+\"/\")\n\t\t\tif relPath == \"\" || relPath == \".\" {\n\t\t\t\tif d.IsDir() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tdstPath, err := tool.SecureJoin(outputPath, relPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif d.IsDir() {\n\t\t\t\treturn os.MkdirAll(dstPath, 0700)\n\t\t\t}\n\t\t\tinfo, err := d.Info()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !info.Mode().IsRegular() {\n\t\t\t\treturn fmt.Errorf(\"%w: %s\", tool.ErrArchiveIllegalPath, p)\n\t\t\t}\n\t\t\tif err := os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn decompress(fsys, p, dstPath, func(_ float64) {})\n\t\t})\n\t} else {\n\t\tentryName := stdpath.Base(path)\n\t\tdstPath, e := tool.SecureJoin(outputPath, entryName)\n\t\tif e != nil {\n\t\t\treturn e\n\t\t}\n\t\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = decompress(fsys, path, dstPath, 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\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/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, dstPath 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\tif !stat.Mode().IsRegular() {\n\t\treturn fmt.Errorf(\"%w: %s\", tool.ErrArchiveIllegalPath, filePath)\n\t}\n\tf, err := os.OpenFile(dstPath, 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\"io\"\n\t\"os\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\toutputPath, err = tool.SecureJoin(outputPath, obj.Name())\n\t\t\tif err != nil {\n\t\t\t\treturn err\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\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/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\treturn decompressEntry(f.Reader(), f.Size(), path, f.Name(), up)\n}\n\nfunc decompressEntry(reader io.Reader, size int64, path, entryName string, up model.UpdateProgress) error {\n\tdstPath, err := tool.SecureJoin(path, entryName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\treturn err\n\t}\n\tfile, err := os.OpenFile(dstPath, 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: reader,\n\t\t\tSize:   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, err := tool.SecureJoin(path, child.Name())\n\t\t\tif err != nil {\n\t\t\t\treturn err\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\"fmt\"\n\t\"io\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\": {\".part%d.rar\", 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\tdstPath, e := tool.SecureJoin(outputPath, name)\n\t\t\tif e != nil {\n\t\t\t\treturn e\n\t\t\t}\n\t\t\terr = decompress(reader, header, dstPath)\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 := stdpath.Base(innerPath)\n\t\tcreatedBaseDir := false\n\t\tvar baseDirPath string\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\tif header.IsDir {\n\t\t\t\t\tif !createdBaseDir {\n\t\t\t\t\t\tbaseDirPath, err = tool.SecureJoin(outputPath, innerBase)\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\tif err = os.MkdirAll(baseDirPath, 0700); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcreatedBaseDir = true\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !header.Mode().IsRegular() {\n\t\t\t\t\treturn fmt.Errorf(\"%w: %s\", tool.ErrArchiveIllegalPath, header.Name)\n\t\t\t\t}\n\t\t\t\tdstPath, e := tool.SecureJoin(outputPath, stdpath.Base(innerPath))\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn e\n\t\t\t\t}\n\t\t\t\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = _decompress(reader, header, dstPath, 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\tif !createdBaseDir {\n\t\t\t\t\tbaseDirPath, err = tool.SecureJoin(outputPath, innerBase)\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 = os.MkdirAll(baseDirPath, 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\tif restPath == \"\" || restPath == \".\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdstPath, e := tool.SecureJoin(baseDirPath, restPath)\n\t\t\t\tif e != nil {\n\t\t\t\t\treturn e\n\t\t\t\t}\n\t\t\t\terr = decompress(reader, header, dstPath)\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\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/nwaples/rardecode/v2\"\n)\n\ntype VolumeFile struct {\n\tstream.SStreamReadAtSeeker\n\tname string\n}\n\nfunc (v *VolumeFile) Name() string {\n\treturn v.name\n}\n\nfunc (v *VolumeFile) Size() int64 {\n\treturn v.SStreamReadAtSeeker.GetRawStream().GetSize()\n}\n\nfunc (v *VolumeFile) Mode() fs.FileMode {\n\treturn 0644\n}\n\nfunc (v *VolumeFile) ModTime() time.Time {\n\treturn v.SStreamReadAtSeeker.GetRawStream().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: {SStreamReadAtSeeker: 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{SStreamReadAtSeeker: reader, name: fileName}\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 stdpath.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, dstPath string) error {\n\tif header.IsDir {\n\t\treturn os.MkdirAll(dstPath, 0700)\n\t}\n\tif !header.Mode().IsRegular() {\n\t\treturn fmt.Errorf(\"%w: %s\", tool.ErrArchiveIllegalPath, header.Name)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\treturn err\n\t}\n\treturn _decompress(reader, header, dstPath, func(_ float64) {})\n}\n\nfunc _decompress(reader *rardecode.Reader, header *rardecode.FileHeader, dstPath string, up model.UpdateProgress) error {\n\tf, err := os.OpenFile(dstPath, 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\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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\": {\".7z.%.3d\", 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\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/bodgit/sevenzip\"\n\t\"io\"\n\t\"io/fs\"\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"io\"\n)\n\ntype MultipartExtension struct {\n\tPartFileFormat  string\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\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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 = stdpath.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 = stdpath.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 = stdpath.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 := stdpath.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 = stdpath.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 = stdpath.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\tinfo := file.FileInfo()\n\t\t\tif info.IsDir() {\n\t\t\t\tvar dirPath string\n\t\t\t\tdirPath, err = SecureJoin(outputPath, name)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif err = os.MkdirAll(dirPath, 0700); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !info.Mode().IsRegular() {\n\t\t\t\treturn fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, name)\n\t\t\t}\n\t\t\tvar dstPath string\n\t\t\tdstPath, err = SecureJoin(outputPath, name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = _decompress(file, dstPath, args.Password, func(_ float64) {})\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 := stdpath.Base(innerPath)\n\t\tcreatedBaseDir := false\n\t\tvar baseDirPath string\n\t\tfor _, file := range files {\n\t\t\tname := file.Name()\n\t\t\tif name == innerPath {\n\t\t\t\tinfo := file.FileInfo()\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\tif !createdBaseDir {\n\t\t\t\t\t\tbaseDirPath, err = SecureJoin(outputPath, innerBase)\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\tif err = os.MkdirAll(baseDirPath, 0700); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcreatedBaseDir = true\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !info.Mode().IsRegular() {\n\t\t\t\t\treturn fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, name)\n\t\t\t\t}\n\t\t\t\tvar dstPath string\n\t\t\t\tdstPath, err = SecureJoin(outputPath, stdpath.Base(innerPath))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = _decompress(file, dstPath, 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\tif !createdBaseDir {\n\t\t\t\t\tbaseDirPath, err = SecureJoin(outputPath, innerBase)\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 = os.MkdirAll(baseDirPath, 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\tif restPath == \"\" || restPath == \".\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tinfo := file.FileInfo()\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\tvar dirPath string\n\t\t\t\t\tdirPath, err = SecureJoin(baseDirPath, restPath)\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\tif err = os.MkdirAll(dirPath, 0700); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !info.Mode().IsRegular() {\n\t\t\t\t\treturn fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, name)\n\t\t\t\t}\n\t\t\t\tvar dstPath string\n\t\t\t\tdstPath, err = SecureJoin(baseDirPath, restPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif err = os.MkdirAll(filepath.Dir(dstPath), 0700); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = _decompress(file, dstPath, args.Password, func(_ float64) {})\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, dstPath, 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\tf, err := os.OpenFile(dstPath, 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/securepath.go",
    "content": "package tool\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// ErrArchiveIllegalPath indicates an archive entry path is unsafe for extraction.\nvar ErrArchiveIllegalPath = errors.New(\"archive entry has illegal path\")\n\n// SecureJoin returns a safe extraction path for an archive entry.\n// It rejects absolute paths, traversal, Windows drive/UNC paths, and NUL bytes.\nfunc SecureJoin(baseDir, entryName string) (string, error) {\n\tif strings.Contains(entryName, \"\\x00\") {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\n\tnormalized := strings.ReplaceAll(entryName, \"\\\\\", \"/\")\n\tif strings.HasPrefix(normalized, \"//\") {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\tcleaned := path.Clean(normalized)\n\n\tif cleaned == \".\" || cleaned == \"..\" || strings.HasPrefix(cleaned, \"../\") {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\tif strings.HasPrefix(cleaned, \"/\") {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\n\trel := filepath.FromSlash(cleaned)\n\tif filepath.IsAbs(rel) || filepath.VolumeName(rel) != \"\" {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\tif strings.HasPrefix(rel, `\\\\`) {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\n\tbase := filepath.Clean(baseDir)\n\tdst := filepath.Join(base, rel)\n\n\tbaseAbs, err := filepath.Abs(base)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: %s (%v)\", ErrArchiveIllegalPath, entryName, err)\n\t}\n\tdstAbs, err := filepath.Abs(dst)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: %s (%v)\", ErrArchiveIllegalPath, entryName, err)\n\t}\n\n\trelCheck, err := filepath.Rel(baseAbs, dstAbs)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: %s (%v)\", ErrArchiveIllegalPath, entryName, err)\n\t}\n\tif relCheck == \"..\" || strings.HasPrefix(relCheck, \"..\"+string(os.PathSeparator)) {\n\t\treturn \"\", fmt.Errorf(\"%w: %s\", ErrArchiveIllegalPath, entryName)\n\t}\n\treturn dst, nil\n}\n"
  },
  {
    "path": "internal/archive/tool/securepath_test.go",
    "content": "package tool\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSecureJoin(t *testing.T) {\n\tbaseDir := t.TempDir()\n\ttests := []struct {\n\t\tname    string\n\t\tentry   string\n\t\twantErr bool\n\t}{\n\t\t{name: \"ok\", entry: \"a/b/c.txt\", wantErr: false},\n\t\t{name: \"parent\", entry: \"../evil.txt\", wantErr: true},\n\t\t{name: \"parent-backslash\", entry: \"..\\\\evil.txt\", wantErr: true},\n\t\t{name: \"abs\", entry: \"/tmp/evil.txt\", wantErr: true},\n\t\t{name: \"drive\", entry: \"C:\\\\evil.txt\", wantErr: true},\n\t\t{name: \"unc\", entry: \"\\\\\\\\server\\\\share\\\\evil.txt\", wantErr: true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdst, err := SecureJoin(baseDir, tc.entry)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"expected error for %q, got nil\", tc.entry)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(err.Error(), tc.entry) {\n\t\t\t\t\tt.Fatalf(\"error should include entry name %q, got %q\", tc.entry, err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error for %q: %v\", tc.entry, err)\n\t\t\t}\n\t\t\trel, err := filepath.Rel(baseDir, dst)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Rel failed: %v\", err)\n\t\t\t}\n\t\t\tif rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\t\t\tt.Fatalf(\"path escaped baseDir: %q\", dst)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/archive/tool/utils.go",
    "content": "package tool\n\nimport (\n\t\"github.com/alist-org/alist/v3/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\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/saintfish/chardet\"\n\t\"github.com/yeka/zip\"\n\t\"golang.org/x/text/encoding\"\n\t\"golang.org/x/text/encoding/charmap\"\n\t\"golang.org/x/text/encoding/japanese\"\n\t\"golang.org/x/text/encoding/korean\"\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n\t\"golang.org/x/text/encoding/traditionalchinese\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/encoding/unicode/utf32\"\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}\n\nfunc (f *WrapFileInfo) Name() string {\n\treturn decodeName(f.FileInfo.Name())\n}\n\ntype WrapFile struct {\n\tf *zip.File\n}\n\nfunc (f *WrapFile) Name() string {\n\treturn decodeName(f.f.Name)\n}\n\nfunc (f *WrapFile) FileInfo() fs.FileInfo {\n\treturn &WrapFileInfo{FileInfo: f.f.FileInfo()}\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 getReader(ss []*stream.SeekableStream) (*zip.Reader, error) {\n\tif len(ss) > 1 && stdpath.Ext(ss[1].GetName()) == \".z01\" {\n\t\t// FIXME: Incorrect parsing method for standard multipart zip format\n\t\tss = append(ss[1:], ss[0])\n\t}\n\treader, err := stream.NewMultiReaderAt(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn zip.NewReader(reader, reader.Size())\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) string {\n\tb := []byte(name)\n\tdetector := chardet.NewTextDetector()\n\tresults, err := detector.DetectAll(b)\n\tif err != nil {\n\t\treturn name\n\t}\n\tvar ce, re, enc encoding.Encoding\n\tfor _, r := range results {\n\t\tif r.Confidence > 30 {\n\t\t\tce = getCommonEncoding(r.Charset)\n\t\t\tif ce != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif re == nil {\n\t\t\tre = getEncoding(r.Charset)\n\t\t}\n\t}\n\tif ce != nil {\n\t\tenc = ce\n\t} else if re != nil {\n\t\tenc = re\n\t} else {\n\t\treturn name\n\t}\n\ti := bytes.NewReader(b)\n\tdecoder := transform.NewReader(i, enc.NewDecoder())\n\tcontent, _ := io.ReadAll(decoder)\n\treturn string(content)\n}\n\nfunc getCommonEncoding(name string) (enc encoding.Encoding) {\n\tswitch name {\n\tcase \"UTF-8\":\n\t\tenc = unicode.UTF8\n\tcase \"UTF-16LE\":\n\t\tenc = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)\n\tcase \"Shift_JIS\":\n\t\tenc = japanese.ShiftJIS\n\tcase \"GB-18030\":\n\t\tenc = simplifiedchinese.GB18030\n\tcase \"EUC-KR\":\n\t\tenc = korean.EUCKR\n\tcase \"Big5\":\n\t\tenc = traditionalchinese.Big5\n\tdefault:\n\t\tenc = nil\n\t}\n\treturn\n}\n\nfunc getEncoding(name string) (enc encoding.Encoding) {\n\tswitch name {\n\tcase \"UTF-8\":\n\t\tenc = unicode.UTF8\n\tcase \"UTF-16BE\":\n\t\tenc = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM)\n\tcase \"UTF-16LE\":\n\t\tenc = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)\n\tcase \"UTF-32BE\":\n\t\tenc = utf32.UTF32(utf32.BigEndian, utf32.IgnoreBOM)\n\tcase \"UTF-32LE\":\n\t\tenc = utf32.UTF32(utf32.LittleEndian, utf32.IgnoreBOM)\n\tcase \"ISO-8859-1\":\n\t\tenc = charmap.ISO8859_1\n\tcase \"ISO-8859-2\":\n\t\tenc = charmap.ISO8859_2\n\tcase \"ISO-8859-3\":\n\t\tenc = charmap.ISO8859_3\n\tcase \"ISO-8859-4\":\n\t\tenc = charmap.ISO8859_4\n\tcase \"ISO-8859-5\":\n\t\tenc = charmap.ISO8859_5\n\tcase \"ISO-8859-6\":\n\t\tenc = charmap.ISO8859_6\n\tcase \"ISO-8859-7\":\n\t\tenc = charmap.ISO8859_7\n\tcase \"ISO-8859-8\":\n\t\tenc = charmap.ISO8859_8\n\tcase \"ISO-8859-8-I\":\n\t\tenc = charmap.ISO8859_8I\n\tcase \"ISO-8859-9\":\n\t\tenc = charmap.ISO8859_9\n\tcase \"windows-1251\":\n\t\tenc = charmap.Windows1251\n\tcase \"windows-1256\":\n\t\tenc = charmap.Windows1256\n\tcase \"KOI8-R\":\n\t\tenc = charmap.KOI8R\n\tcase \"Shift_JIS\":\n\t\tenc = japanese.ShiftJIS\n\tcase \"GB-18030\":\n\t\tenc = simplifiedchinese.GB18030\n\tcase \"EUC-JP\":\n\t\tenc = japanese.EUCJP\n\tcase \"EUC-KR\":\n\t\tenc = korean.EUCKR\n\tcase \"Big5\":\n\t\tenc = traditionalchinese.Big5\n\tcase \"ISO-2022-JP\":\n\t\tenc = japanese.ISO2022JP\n\tdefault:\n\t\tenc = nil\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/archive/zip/zip.go",
    "content": "package zip\n\nimport (\n\t\"io\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n)\n\ntype Zip struct {\n}\n\nfunc (Zip) AcceptedExtensions() []string {\n\treturn []string{}\n}\n\nfunc (Zip) AcceptedMultipartExtensions() map[string]tool.MultipartExtension {\n\treturn map[string]tool.MultipartExtension{\n\t\t\".zip\":     {\".z%.2d\", 1},\n\t\t\".zip.001\": {\".zip.%.3d\", 2},\n\t}\n}\n\nfunc (Zip) GetMeta(ss []*stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {\n\tzipReader, err := getReader(ss)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tencrypted, tree := tool.GenerateMetaTreeFromFolderTraversal(&WrapReader{Reader: zipReader})\n\treturn &model.ArchiveMetaInfo{\n\t\tComment:   zipReader.Comment,\n\t\tEncrypted: encrypted,\n\t\tTree:      tree,\n\t}, nil\n}\n\nfunc (Zip) List(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {\n\tzipReader, err := 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), \"/\")\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()}))\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)\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()}))\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 (Zip) Extract(ss []*stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {\n\tzipReader, err := 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) == 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 (Zip) Decompress(ss []*stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {\n\tzipReader, err := 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}\n"
  },
  {
    "path": "internal/authn/authn.go",
    "content": "package authn\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n)\n\nfunc NewAuthnInstance(r *http.Request) (*webauthn.WebAuthn, error) {\n\tsiteUrl, err := url.Parse(common.GetApiUrl(r))\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/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/net\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/caarlos0/env/v9\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc InitConfig() {\n\tif flags.ForceBinDir {\n\t\tif !filepath.IsAbs(flags.DataDir) {\n\t\t\tex, err := os.Executable()\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Fatal(err)\n\t\t\t}\n\t\t\texPath := filepath.Dir(ex)\n\t\t\tflags.DataDir = filepath.Join(exPath, flags.DataDir)\n\t\t}\n\t}\n\tconfigPath := filepath.Join(flags.DataDir, \"config.json\")\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()\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()\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.MaxConcurrency > 0 {\n\t\tnet.DefaultConcurrencyLimit = &net.ConcurrencyLimit{Limit: conf.Conf.MaxConcurrency}\n\t}\n\tif !conf.Conf.Force {\n\t\tconfFromEnv()\n\t}\n\tif conf.Conf.TlsInsecureSkipVerify {\n\t\tlog.Warn(\"SECURITY WARNING / 安全警告:\")\n\t\tlog.Warn(\"TLS certificate verification is disabled.\")\n\t\tlog.Warn(\"TLS 证书校验已被禁用。\")\n\t\tlog.Warn(\"This exposes all storage traffic to MitM attacks and may leak credentials or allow data tampering.\")\n\t\tlog.Warn(\"这会使所有存储通信暴露于中间人攻击（MitM），可能导致凭据泄露和数据被篡改。\")\n\t\tlog.Warn(\"Only use this setting if you fully understand the risks.\")\n\t\tlog.Warn(\"仅在你完全理解风险的情况下使用该配置。\")\n\t}\n\t// convert abs path\n\tif !filepath.IsAbs(conf.Conf.TempDir) {\n\t\tabsPath, err := filepath.Abs(conf.Conf.TempDir)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get abs path error: %+v\", err)\n\t\t}\n\t\tconf.Conf.TempDir = absPath\n\t}\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\tbase.InitClient()\n\tinitURL()\n}\n\nfunc confFromEnv() {\n\tprefix := \"ALIST_\"\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"
  },
  {
    "path": "internal/bootstrap/data/data.go",
    "content": "package data\n\nimport \"github.com/alist-org/alist/v3/cmd/flags\"\n\nfunc InitData() {\n\tinitRoles()\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/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/message\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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:       nil,\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/role.go",
    "content": "package data\n\n// initRoles creates the default admin and guest roles if missing.\n// These roles are essential and must not be modified or removed.\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nfunc initRoles() {\n\tguestRole, err := op.GetRoleByName(\"guest\")\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tguestRole = &model.Role{\n\t\t\t\tID:          uint(model.GUEST),\n\t\t\t\tName:        \"guest\",\n\t\t\t\tDescription: \"Guest\",\n\t\t\t\tPermissionScopes: []model.PermissionEntry{\n\t\t\t\t\t{Path: \"/\", Permission: 0},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err := op.CreateRole(guestRole); err != nil {\n\t\t\t\tutils.Log.Fatalf(\"[init role] Failed to create guest role: %v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Fatalf(\"[init role] Failed to get guest role: %v\", err)\n\t\t}\n\t}\n\n\t_, err = op.GetRoleByName(\"admin\")\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tadminRole := &model.Role{\n\t\t\t\tID:          uint(model.ADMIN),\n\t\t\t\tName:        \"admin\",\n\t\t\t\tDescription: \"Administrator\",\n\t\t\t\tPermissionScopes: []model.PermissionEntry{\n\t\t\t\t\t{Path: \"/\", Permission: 0xFFFF},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err := op.CreateRole(adminRole); err != nil {\n\t\t\t\tutils.Log.Fatalf(\"[init role] Failed to create admin role: %v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Fatalf(\"[init role] Failed to get admin role: %v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bootstrap/data/setting.go",
    "content": "package data\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nvar initialSettingItems []model.SettingItem\n\nfunc initSettings() {\n\tInitialSettings()\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\t// create or save setting\n\tsave := false\n\tfor i := range initialSettingItems {\n\t\titem := &initialSettingItems[i]\n\t\titem.Index = uint(i)\n\t\tif item.PreDefault == \"\" {\n\t\t\titem.PreDefault = item.Value\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 stored != nil && item.Key != conf.VERSION && stored.Value != item.PreDefault {\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\t// save\n\t\tif stored == nil || *item != *stored {\n\t\t\tsave = true\n\t\t}\n\t}\n\tif save {\n\t\terr = db.SaveSettingItems(initialSettingItems)\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 isActive(key string) bool {\n\tfor _, item := range initialSettingItems {\n\t\tif item.Key == key {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\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\tdefaultRoleID := strconv.Itoa(model.GUEST)\n\tinitialSettingItems = []model.SettingItem{\n\t\t// site settings\n\t\t{Key: conf.VERSION, Value: conf.Version, 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: \"AList\", Type: conf.TypeString, Group: model.SITE},\n\t\t{Key: conf.Announcement, Value: \"### repo\\nhttps://github.com/alist-org/alist\", 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: \"50\", 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{Key: conf.AllowRegister, Value: \"false\", Type: conf.TypeBool, Group: model.SITE},\n\t\t{Key: conf.DefaultRole, Value: defaultRoleID, Type: conf.TypeSelect, Group: model.SITE},\n\t\t// newui settings\n\t\t{Key: conf.UseNewui, Value: \"false\", Type: conf.TypeBool, Group: model.SITE},\n\t\t// style settings\n\t\t{Key: conf.Logo, Value: \"https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg\", Type: conf.TypeText, Group: model.STYLE},\n\t\t{Key: conf.Favicon, Value: \"https://cdn.jsdelivr.net/gh/alist-org/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: \"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// 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\", 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\", 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://alist-org.github.io/pdf.js/web/viewer.html?file=$e_url\"\n\t},\n\t\"epub\": {\n\t\t\"EPUB.js\":\"https://alist-org.github.io/static/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://alist-org.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://jsd.nn.ci/gh/alist-org/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.ThumbnailSize, Value: \"144\", Type: conf.TypeNumber, Group: model.PREVIEW, Help: \"Thumbnail width in pixels. Height is scaled proportionally.\"},\n\t\t{Key: conf.PreviewArchivesByDefault, Value: \"true\", 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// 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, PreDefault: `<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{Key: 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{Key: conf.OcrApi, Value: \"https://api.alistgo.com/ocr/file/json\", Type: conf.TypeString, Group: model.GLOBAL},\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,alist_ts\", 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.MaxDevices, Value: \"0\", Type: conf.TypeNumber, Group: model.GLOBAL},\n\t\t{Key: conf.DeviceEvictPolicy, Value: \"deny\", Type: conf.TypeSelect, Options: \"deny,evict_oldest\", Group: model.GLOBAL},\n\t\t{Key: conf.DeviceSessionTTL, Value: \"86400\", Type: conf.TypeNumber, Group: model.GLOBAL},\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.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.FTPProxyUserAgent, Value: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \" +\n\t\t\t\"Chrome/87.0.4280.88 Safari/537.36\", Type: conf.TypeString, 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\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\tinitialSettingItems = append(initialSettingItems, tool.Tools.Items()...)\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/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/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: \"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\"github.com/alist-org/alist/v3/internal/db\"\n\t\"os\"\n\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\nfunc initUser() {\n\tguest, 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\tguestRole, _ := op.GetRoleByName(\"guest\")\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.Roles{int(guestRole.ID)},\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\tadmin, err := op.GetAdmin()\n\tadminPassword := random.String(8)\n\tenvpass := os.Getenv(\"ALIST_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\tadminRole, _ := op.GetRoleByName(\"admin\")\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.Roles{int(adminRole.ID)},\n\t\t\t\tBasePath: \"/\",\n\t\t\t\tAuthn:    \"[]\",\n\t\t\t\t// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)\n\t\t\t\tPermission: 0xFFFF,\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\tutils.Log.Infof(\"Successfully created the admin user and the initial password is: %s\", 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}\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/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/natefinch/lumberjack\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc init() {\n\tformatter := logrus.TextFormatter{\n\t\tTimestampFormat: \"2006-01-02 15:04:05\",\n\t\tFullTimestamp:   true,\n\t}\n\tif os.Getenv(\"NO_COLOR\") != \"\" || os.Getenv(\"ALIST_NO_COLOR\") == \"1\" {\n\t\tformatter.DisableColors = true\n\t} else {\n\t\tformatter.ForceColors = true\n\t\tformatter.EnvironmentOverrideColors = 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}\n"
  },
  {
    "path": "internal/bootstrap/offline_download.go",
    "content": "package bootstrap\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/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 tool %s failed: %s\", k, err)\n\t\t} else {\n\t\t\tutils.Log.Infof(\"init 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/alist-org/alist/v3/internal/bootstrap/patch/v3_24_0\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_32_0\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_41_0\"\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0\"\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: \"v3.46.0\",\n\t\tPatches: []func(){\n\t\t\tv3_46_0.ConvertLegacyRoles,\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/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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.Fatalf(\"[hash pwd for old version] failed get users: %v\", err)\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.Fatalf(\"[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/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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.Fatalf(\"[update authn for old version] failed get users: %v\", err)\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.Fatalf(\"[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/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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 to PR AlistGo/alist#7705 and\n// PR AlistGo/alist#7817.\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/v3_46_0/convert_role.go",
    "content": "package v3_46_0\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"gorm.io/gorm\"\n)\n\n// ConvertLegacyRoles migrates old integer role values to a new role model with permission scopes.\nfunc ConvertLegacyRoles() {\n\tguestRole, err := op.GetRoleByName(\"guest\")\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tguestRole = &model.Role{\n\t\t\t\tID:          uint(model.GUEST),\n\t\t\t\tName:        \"guest\",\n\t\t\t\tDescription: \"Guest\",\n\t\t\t\tPermissionScopes: []model.PermissionEntry{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:       \"/\",\n\t\t\t\t\t\tPermission: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err = op.CreateRole(guestRole); err != nil {\n\t\t\t\tutils.Log.Errorf(\"[convert roles] failed to create guest role: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Errorf(\"[convert roles] failed to get guest role: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tadminRole, err := op.GetRoleByName(\"admin\")\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tadminRole = &model.Role{\n\t\t\t\tID:          uint(model.ADMIN),\n\t\t\t\tName:        \"admin\",\n\t\t\t\tDescription: \"Administrator\",\n\t\t\t\tPermissionScopes: []model.PermissionEntry{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:       \"/\",\n\t\t\t\t\t\tPermission: 0x33FF,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err = op.CreateRole(adminRole); err != nil {\n\t\t\t\tutils.Log.Errorf(\"[convert roles] failed to create admin role: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Errorf(\"[convert roles] failed to get admin role: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tgeneralRole, err := op.GetRoleByName(\"general\")\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tgeneralRole = &model.Role{\n\t\t\t\tID:          uint(model.NEWGENERAL),\n\t\t\t\tName:        \"general\",\n\t\t\t\tDescription: \"General User\",\n\t\t\t\tPermissionScopes: []model.PermissionEntry{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:       \"/\",\n\t\t\t\t\t\tPermission: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err = op.CreateRole(generalRole); err != nil {\n\t\t\t\tutils.Log.Errorf(\"[convert roles] failed create general role: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tutils.Log.Errorf(\"[convert roles] failed get general role: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\trawDb := db.GetDb()\n\ttable := conf.Conf.Database.TablePrefix + \"users\"\n\trows, err := rawDb.Table(table).Select(\"id, username, role\").Rows()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[convert roles] failed to get users: %v\", err)\n\t\treturn\n\t}\n\tdefer rows.Close()\n\n\tvar updatedCount int\n\tfor rows.Next() {\n\t\tvar id uint\n\t\tvar username string\n\t\tvar rawRole []byte\n\n\t\tif err := rows.Scan(&id, &username, &rawRole); err != nil {\n\t\t\tutils.Log.Warnf(\"[convert roles] skip user scan err: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tutils.Log.Debugf(\"[convert roles] user: %s raw role: %s\", username, string(rawRole))\n\n\t\tif len(rawRole) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar oldRoles []int\n\t\twasSingleInt := false\n\t\tif err := json.Unmarshal(rawRole, &oldRoles); err != nil {\n\t\t\tvar single int\n\t\t\tif err := json.Unmarshal(rawRole, &single); err != nil {\n\t\t\t\tutils.Log.Warnf(\"[convert roles] user %s has invalid role: %s\", username, string(rawRole))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toldRoles = []int{single}\n\t\t\twasSingleInt = true\n\t\t}\n\n\t\tvar newRoles model.Roles\n\t\tfor _, r := range oldRoles {\n\t\t\tswitch r {\n\t\t\tcase model.ADMIN:\n\t\t\t\tnewRoles = append(newRoles, int(adminRole.ID))\n\t\t\tcase model.GUEST:\n\t\t\t\tnewRoles = append(newRoles, int(guestRole.ID))\n\t\t\tcase model.GENERAL:\n\t\t\t\tnewRoles = append(newRoles, int(generalRole.ID))\n\t\t\tdefault:\n\t\t\t\tnewRoles = append(newRoles, r)\n\t\t\t}\n\t\t}\n\n\t\tif wasSingleInt {\n\t\t\terr := rawDb.Table(table).Where(\"id = ?\", id).Update(\"role\", newRoles).Error\n\t\t\tif err != nil {\n\t\t\t\tutils.Log.Errorf(\"[convert roles] failed to update user %s: %v\", username, err)\n\t\t\t} else {\n\t\t\t\tupdatedCount++\n\t\t\t\tutils.Log.Infof(\"[convert roles] updated user %s: %v → %v\", username, oldRoles, newRoles)\n\t\t\t}\n\t\t}\n\t}\n\n\tutils.Log.Infof(\"[convert roles] completed role conversion for %d users\", updatedCount)\n}\n\nfunc IsLegacyRoleDetected() bool {\n\trawDb := db.GetDb()\n\ttable := conf.Conf.Database.TablePrefix + \"users\"\n\trows, err := rawDb.Table(table).Select(\"role\").Rows()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"[role check] failed to scan user roles: %v\", err)\n\t\treturn false\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar raw sql.RawBytes\n\t\tif err := rows.Scan(&raw); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(raw) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar roles []int\n\t\tif err := json.Unmarshal(raw, &roles); err == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar single int\n\t\tif err := json.Unmarshal(raw, &single); err == nil {\n\t\t\tutils.Log.Infof(\"[role check] detected legacy int role: %d\", single)\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/bootstrap/patch.go",
    "content": "package bootstrap\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/internal/bootstrap/patch\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"strings\"\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/storage.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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.StoragesLoaded = true\n\t}(storages)\n}\n"
  },
  {
    "path": "internal/bootstrap/stream_limit.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/xhofe/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.CopyTask](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\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\tworkers := conf.Conf.Tasks.S3Transition.Workers\n\tif workers < 0 {\n\t\tworkers = 0\n\t}\n\tfs.S3TransitionTaskManager = tache.NewManager[*fs.S3TransitionTask](\n\t\ttache.WithWorks(workers),\n\t\ttache.WithPersistFunction(\n\t\t\tdb.GetTaskDataFunc(\"s3_transition\", conf.Conf.Tasks.S3Transition.TaskPersistant),\n\t\t\tdb.UpdateTaskDataFunc(\"s3_transition\", conf.Conf.Tasks.S3Transition.TaskPersistant),\n\t\t),\n\t\ttache.WithMaxRetry(conf.Conf.Tasks.S3Transition.MaxRetry),\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/conf/config.go",
    "content": "package conf\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/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\tIndexPrefix string `json:\"index_prefix\" env:\"INDEX_PREFIX\"`\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}\n\ntype LogConfig struct {\n\tEnable     bool   `json:\"enable\" env:\"LOG_ENABLE\"`\n\tName       string `json:\"name\" env:\"LOG_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}\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\tDecompress         TaskConfig `json:\"decompress\" envPrefix:\"DECOMPRESS_\"`\n\tDecompressUpload   TaskConfig `json:\"decompress_upload\" envPrefix:\"DECOMPRESS_UPLOAD_\"`\n\tS3Transition       TaskConfig `json:\"s3_transition\" envPrefix:\"S3_TRANSITION_\"`\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\"`\n\tDelayedStart          int         `json:\"delayed_start\" env:\"DELAYED_START\"`\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}\n\nfunc DefaultConfig() *Config {\n\ttempDir := filepath.Join(flags.DataDir, \"temp\")\n\tindexDir := filepath.Join(flags.DataDir, \"bleve\")\n\tlogPath := filepath.Join(flags.DataDir, \"log/log.log\")\n\tdbPath := filepath.Join(flags.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},\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},\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\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\tS3Transition: 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\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}\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\tAllowRegister = \"allow_register\"\n\tDefaultRole   = \"default_role\"\n\tUseNewui      = \"use_newui\"\n\n\tLogo      = \"logo\"\n\tFavicon   = \"favicon\"\n\tMainColor = \"main_color\"\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\tThumbnailSize            = \"thumbnail_size\"\n\tPreviewArchivesByDefault = \"preview_archives_by_default\"\n\tReadMeAutoRender         = \"readme_autorender\"\n\tFilterReadMeScripts      = \"filter_readme_scripts\"\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\tMaxDevices              = \"max_devices\"\n\tDeviceEvictPolicy       = \"device_evict_policy\"\n\tDeviceSessionTTL        = \"device_session_ttl\"\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// pikpak\n\tPikPakTempDir = \"pikpak_temp_dir\"\n\n\t// thunder\n\tThunderTempDir = \"thunder_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\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// ftp\n\tFTPPublicHost        = \"ftp_public_host\"\n\tFTPPasvPortMap       = \"ftp_pasv_port_map\"\n\tFTPProxyUserAgent    = \"ftp_proxy_user_agent\"\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\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\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.\nconst (\n\tNoTaskKey = \"no_task\"\n)\n"
  },
  {
    "path": "internal/conf/var.go",
    "content": "package conf\n\nimport (\n\t\"net/url\"\n\t\"regexp\"\n)\n\nvar (\n\tBuiltAt    string\n\tGitAuthor  string\n\tGitCommit  string\n\tVersion    string = \"dev\"\n\tWebVersion string\n)\n\nvar (\n\tConf *Config\n\tURL  *url.URL\n)\n\nvar SlicesMap = make(map[string][]string)\nvar FilenameCharMap = make(map[string]string)\nvar PrivacyReg []*regexp.Regexp\n\nvar (\n\t// StoragesLoaded loaded success if empty\n\tStoragesLoaded = false\n)\nvar (\n\tRawIndexHtml string\n\tManageHtml   string\n\tIndexHtml    string\n)\n"
  },
  {
    "path": "internal/db/db.go",
    "content": "package db\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/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.Role), new(model.Label), new(model.LabelFileBinding), new(model.ObjFile), new(model.Session))\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/label.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n\t\"time\"\n)\n\n// GetLabels Get all label from database order by id\nfunc GetLabels(pageIndex, pageSize int) ([]model.Label, int64, error) {\n\tlabelDB := db.Model(&model.Label{})\n\tvar count int64\n\tif err := labelDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get label count\")\n\t}\n\tvar labels []model.Label\n\tif err := labelDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&labels).Error; err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\treturn labels, count, nil\n}\n\n// GetLabelById Get Label by id, used to update label usually\nfunc GetLabelById(id uint) (*model.Label, error) {\n\tvar label model.Label\n\tlabel.ID = id\n\tif err := db.First(&label).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn &label, nil\n}\n\n// CreateLabel just insert label to database\nfunc CreateLabel(label model.Label) (uint, error) {\n\tlabel.CreateTime = time.Now()\n\terr := errors.WithStack(db.Create(&label).Error)\n\tif err != nil {\n\t\treturn label.ID, errors.WithMessage(err, \"failed create label in database\")\n\t}\n\treturn label.ID, nil\n}\n\n// UpdateLabel just update storage in database\nfunc UpdateLabel(label *model.Label) (*model.Label, error) {\n\tlabel.CreateTime = time.Now()\n\t_, err := GetLabelById(label.ID)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get old label\")\n\t}\n\terr = errors.WithStack(db.Save(label).Error)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed create label in database\")\n\t}\n\treturn label, nil\n}\n\n// DeleteLabelById just delete label from database by id\nfunc DeleteLabelById(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.Label{}, id).Error)\n}\n\n// GetLabelByIds Get label from database order by ids\nfunc GetLabelByIds(ids []uint) ([]model.Label, error) {\n\tlabelDB := db.Model(&model.Label{})\n\tvar labels []model.Label\n\tif err := labelDB.Where(ids).Find(&labels).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn labels, nil\n}\n\n// GetLabelByName Get Label by name\nfunc GetLabelByName(name string) bool {\n\tvar label model.Label\n\tresult := db.Where(\"name = ?\", name).First(&label)\n\texists := !errors.Is(result.Error, gorm.ErrRecordNotFound)\n\treturn exists\n}\n"
  },
  {
    "path": "internal/db/label_file_binding.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"time\"\n)\n\n// GetLabelIds Get all label_ids from database order by file_name\nfunc GetLabelIds(userId uint, fileName string) ([]uint, error) {\n\t//fmt.Printf(\">>> [GetLabelIds] userId: %d, fileName: %s\\n\", userId, fileName)\n\tlabelFileBinDingDB := db.Model(&model.LabelFileBinding{})\n\tvar labelIds []uint\n\tif err := labelFileBinDingDB.Where(\"file_name = ?\", fileName).Where(\"user_id = ?\", userId).Pluck(\"label_id\", &labelIds).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn labelIds, nil\n}\n\nfunc CreateLabelFileBinDing(fileName string, labelId, userId uint) error {\n\tvar labelFileBinDing model.LabelFileBinding\n\tlabelFileBinDing.UserId = userId\n\tlabelFileBinDing.LabelId = labelId\n\tlabelFileBinDing.FileName = fileName\n\tlabelFileBinDing.CreateTime = time.Now()\n\terr := errors.WithStack(db.Create(&labelFileBinDing).Error)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed create label in database\")\n\t}\n\treturn nil\n}\n\n// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually\nfunc GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {\n\tvar labelFileBinDing model.LabelFileBinding\n\tresult := db.Where(\"label_id = ?\", labelId).Where(\"user_id = ?\", userId).First(&labelFileBinDing)\n\texists := !errors.Is(result.Error, gorm.ErrRecordNotFound)\n\treturn exists\n}\n\n// DelLabelFileBinDingByFileName used to del usually\nfunc DelLabelFileBinDingByFileName(userId uint, fileName string) error {\n\treturn errors.WithStack(db.Where(\"file_name = ?\", fileName).Where(\"user_id = ?\", userId).Delete(model.LabelFileBinding{}).Error)\n}\n\n// DelLabelFileBinDingById used to del usually\nfunc DelLabelFileBinDingById(labelId, userId uint, fileName string) error {\n\treturn errors.WithStack(db.Where(\"label_id = ?\", labelId).Where(\"file_name = ?\", fileName).Where(\"user_id = ?\", userId).Delete(model.LabelFileBinding{}).Error)\n}\n\nfunc GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinding, err error) {\n\tif err := db.Where(\"label_id in (?)\", labelIds).Where(\"user_id = ?\", userId).Find(&result).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn result, nil\n}\n\nfunc GetLabelBindingsByFileNamesPublic(fileNames []string) (map[string][]uint, error) {\n\tvar binds []model.LabelFileBinding\n\tif err := db.Where(\"file_name IN ?\", fileNames).Find(&binds).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tout := make(map[string][]uint, len(fileNames))\n\tseen := make(map[string]struct{}, len(binds))\n\tfor _, b := range binds {\n\t\tkey := fmt.Sprintf(\"%s-%d\", b.FileName, b.LabelId)\n\t\tif _, ok := seen[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tout[b.FileName] = append(out[b.FileName], b.LabelId)\n\t}\n\treturn out, nil\n}\n\nfunc GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {\n\tbindMap, err := GetLabelBindingsByFileNamesPublic(fileNames)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidSet := make(map[uint]struct{})\n\tfor _, ids := range bindMap {\n\t\tfor _, id := range ids {\n\t\t\tidSet[id] = struct{}{}\n\t\t}\n\t}\n\tif len(idSet) == 0 {\n\t\treturn make(map[string][]model.Label, 0), nil\n\t}\n\tallIDs := make([]uint, 0, len(idSet))\n\tfor id := range idSet {\n\t\tallIDs = append(allIDs, id)\n\t}\n\tlabels, err := GetLabelByIds(allIDs) // 你已有的函数\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlabelByID := make(map[uint]model.Label, len(labels))\n\tfor _, l := range labels {\n\t\tlabelByID[l.ID] = l\n\t}\n\n\tout := make(map[string][]model.Label, len(bindMap))\n\tfor fname, ids := range bindMap {\n\t\tfor _, id := range ids {\n\t\t\tif lab, ok := labelByID[id]; ok {\n\t\t\t\tout[fname] = append(out[fname], lab)\n\t\t\t}\n\t\t}\n\t}\n\treturn out, nil\n}\n\nfunc ListLabelFileBinDing(userId uint, labelIDs []uint, fileName string, page, pageSize int) ([]model.LabelFileBinding, int64, error) {\n\tq := db.Model(&model.LabelFileBinding{}).Where(\"user_id = ?\", userId)\n\n\tif len(labelIDs) > 0 {\n\t\tq = q.Where(\"label_id IN ?\", labelIDs)\n\t}\n\tif fileName != \"\" {\n\t\tq = q.Where(\"file_name LIKE ?\", \"%\"+fileName+\"%\")\n\t}\n\n\tvar total int64\n\tif err := q.Count(&total).Error; err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\n\tvar rows []model.LabelFileBinding\n\tif err := q.\n\t\tOrder(\"id DESC\").\n\t\tOffset((page - 1) * pageSize).\n\t\tLimit(pageSize).\n\t\tFind(&rows).Error; err != nil {\n\t\treturn nil, 0, errors.WithStack(err)\n\t}\n\treturn rows, total, nil\n}\n\nfunc RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {\n\tif len(bindings) == 0 {\n\t\treturn nil\n\t}\n\ttx := db.Begin()\n\n\tif override {\n\t\ttype key struct {\n\t\t\tuid  uint\n\t\t\tname string\n\t\t}\n\t\ttoDel := make(map[key]struct{}, len(bindings))\n\t\tfor i := range bindings {\n\t\t\tk := key{uid: bindings[i].UserId, name: bindings[i].FileName}\n\t\t\ttoDel[k] = struct{}{}\n\t\t}\n\t\tfor k := range toDel {\n\t\t\tif err := tx.Where(\"user_id = ? AND file_name = ?\", k.uid, k.name).\n\t\t\t\tDelete(&model.LabelFileBinding{}).Error; err != nil {\n\t\t\t\ttx.Rollback()\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := range bindings {\n\t\tb := bindings[i]\n\t\tif !keepIDs {\n\t\t\tb.ID = 0\n\t\t}\n\t\tif b.CreateTime.IsZero() {\n\t\t\tb.CreateTime = time.Now()\n\t\t}\n\t\tif override {\n\t\t\tif err := tx.Create(&b).Error; err != nil {\n\t\t\t\ttx.Rollback()\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&b).Error; err != nil {\n\t\t\t\ttx.Rollback()\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors.WithStack(tx.Commit().Error)\n}\n"
  },
  {
    "path": "internal/db/meta.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/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/obj_file.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\n// GetFileByNameExists Get file by name\nfunc GetFileByNameExists(name string) bool {\n\tvar label model.ObjFile\n\tresult := db.Where(\"name = ?\", name).First(&label)\n\texists := !errors.Is(result.Error, gorm.ErrRecordNotFound)\n\treturn exists\n}\n\n// GetFileByName Get file by name\nfunc GetFileByName(name string, userId uint) (objFile model.ObjFile, err error) {\n\tif err = db.Where(\"name = ?\", name).Where(\"user_id = ?\", userId).First(&objFile).Error; err != nil {\n\t\treturn objFile, errors.WithStack(err)\n\t}\n\treturn objFile, nil\n}\n\nfunc CreateObjFile(obj model.ObjFile) error {\n\terr := errors.WithStack(db.Create(&obj).Error)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed create file in database\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/db/role.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"path\"\n\t\"strings\"\n)\n\nfunc GetRole(id uint) (*model.Role, error) {\n\tvar r model.Role\n\tif err := db.First(&r, id).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get role\")\n\t}\n\treturn &r, nil\n}\n\nfunc GetRoleByName(name string) (*model.Role, error) {\n\tr := model.Role{Name: name}\n\tif err := db.Where(r).First(&r).Error; err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed get role\")\n\t}\n\treturn &r, nil\n}\n\nfunc GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err error) {\n\troleDB := db.Model(&model.Role{})\n\tif err = roleDB.Count(&count).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get roles count\")\n\t}\n\tif err = roleDB.Order(columnName(\"id\")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&roles).Error; err != nil {\n\t\treturn nil, 0, errors.Wrapf(err, \"failed get find roles\")\n\t}\n\treturn roles, count, nil\n}\n\nfunc GetAllRoles() ([]model.Role, error) {\n\tvar roles []model.Role\n\tif err := db.Find(&roles).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn roles, nil\n}\n\nfunc CreateRole(r *model.Role) error {\n\tif err := db.Create(r).Error; err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif r.Default {\n\t\tif err := db.Model(&model.Role{}).Where(\"id <> ?\", r.ID).Update(\"default\", false).Error; err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc UpdateRole(r *model.Role) error {\n\tif err := db.Save(r).Error; err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif r.Default {\n\t\tif err := db.Model(&model.Role{}).Where(\"id <> ?\", r.ID).Update(\"default\", false).Error; err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc DeleteRole(id uint) error {\n\treturn errors.WithStack(db.Delete(&model.Role{}, id).Error)\n}\n\nfunc UpdateRolePermissionsPathPrefix(oldPath, newPath string) ([]uint, error) {\n\tvar roles []model.Role\n\tvar modifiedRoleIDs []uint\n\n\tif err := db.Find(&roles).Error; err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed to load roles\")\n\t}\n\n\tfor _, role := range roles {\n\t\tif role.Name == \"admin\" || role.Name == \"guest\" {\n\t\t\tcontinue\n\t\t}\n\t\tupdated := false\n\t\tfor i, entry := range role.PermissionScopes {\n\t\t\tentryPath := path.Clean(entry.Path)\n\t\t\toldPathClean := path.Clean(oldPath)\n\n\t\t\tif entryPath == oldPathClean {\n\t\t\t\trole.PermissionScopes[i].Path = newPath\n\t\t\t\tupdated = true\n\t\t\t} else if strings.HasPrefix(entryPath, oldPathClean+\"/\") {\n\t\t\t\trole.PermissionScopes[i].Path = newPath + entryPath[len(oldPathClean):]\n\t\t\t\tupdated = true\n\t\t\t}\n\t\t}\n\t\tif updated {\n\t\t\tif err := UpdateRole(&role); err != nil {\n\t\t\t\treturn nil, errors.WithMessagef(err, \"failed to update role ID %d\", role.ID)\n\t\t\t}\n\t\t\tmodifiedRoleIDs = append(modifiedRoleIDs, role.ID)\n\t\t}\n\t}\n\treturn modifiedRoleIDs, nil\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/session.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm/clause\"\n)\n\nfunc GetSession(userID uint, deviceKey string) (*model.Session, error) {\n\ts := model.Session{UserID: userID, DeviceKey: deviceKey}\n\tif err := db.Select(\"user_id, device_key, last_active, status, user_agent, ip\").Where(&s).First(&s).Error; err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed find session\")\n\t}\n\treturn &s, nil\n}\n\nfunc CreateSession(s *model.Session) error {\n\treturn errors.WithStack(db.Create(s).Error)\n}\n\nfunc UpsertSession(s *model.Session) error {\n\treturn errors.WithStack(db.Clauses(clause.OnConflict{UpdateAll: true}).Create(s).Error)\n}\n\nfunc DeleteSession(userID uint, deviceKey string) error {\n\treturn errors.WithStack(db.Where(\"user_id = ? AND device_key = ?\", userID, deviceKey).Delete(&model.Session{}).Error)\n}\n\nfunc CountActiveSessionsByUser(userID uint) (int64, error) {\n\tvar count int64\n\terr := db.Model(&model.Session{}).\n\t\tWhere(\"user_id = ? AND status = ?\", userID, model.SessionActive).\n\t\tCount(&count).Error\n\treturn count, errors.WithStack(err)\n}\n\nfunc DeleteSessionsBefore(ts int64) error {\n\treturn errors.WithStack(db.Where(\"last_active < ?\", ts).Delete(&model.Session{}).Error)\n}\n\n// GetOldestActiveSession returns the oldest active session for the specified user.\nfunc GetOldestActiveSession(userID uint) (*model.Session, error) {\n\tvar s model.Session\n\tif err := db.Where(\"user_id = ? AND status = ?\", userID, model.SessionActive).\n\t\tOrder(\"last_active ASC\").First(&s).Error; err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed get oldest active session\")\n\t}\n\treturn &s, nil\n}\n\nfunc UpdateSessionLastActive(userID uint, deviceKey string, lastActive int64) error {\n\treturn errors.WithStack(db.Model(&model.Session{}).Where(\"user_id = ? AND device_key = ?\", userID, deviceKey).Update(\"last_active\", lastActive).Error)\n}\n\nfunc ListSessionsByUser(userID uint) ([]model.Session, error) {\n\tvar sessions []model.Session\n\terr := db.Select(\"user_id, device_key, last_active, status, user_agent, ip\").Where(\"user_id = ? AND status = ?\", userID, model.SessionActive).Find(&sessions).Error\n\treturn sessions, errors.WithStack(err)\n}\n\nfunc ListSessions() ([]model.Session, error) {\n\tvar sessions []model.Session\n\terr := db.Select(\"user_id, device_key, last_active, status, user_agent, ip\").Where(\"status = ?\", model.SessionActive).Find(&sessions).Error\n\treturn sessions, errors.WithStack(err)\n}\n\nfunc MarkInactive(sessionID string) error {\n\treturn errors.WithStack(db.Model(&model.Session{}).Where(\"device_key = ?\", sessionID).Update(\"status\", model.SessionInactive).Error)\n}\n"
  },
  {
    "path": "internal/db/settingitem.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/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/sshkey.go",
    "content": "package db\n\nimport (\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/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\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\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/go-webauthn/webauthn/webauthn\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n)\n\nfunc GetUserByRole(role int) (*model.User, error) {\n\tvar users []model.User\n\tif err := db.Find(&users).Error; err != nil {\n\t\treturn nil, err\n\t}\n\tfor i := range users {\n\t\tif users[i].Role.Contains(role) {\n\t\t\treturn &users[i], nil\n\t\t}\n\t}\n\treturn nil, gorm.ErrRecordNotFound\n}\n\nfunc GetUsersByRole(roleID int) ([]model.User, error) {\n\tvar users []model.User\n\tif err := db.Find(&users).Error; err != nil {\n\t\treturn nil, err\n\t}\n\tvar result []model.User\n\tfor _, u := range users {\n\t\tif slices.Contains(u.Role, roleID) {\n\t\t\tresult = append(result, u)\n\t\t}\n\t}\n\treturn result, 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 GetAllUsers() ([]model.User, error) {\n\tvar users []model.User\n\tif err := db.Find(&users).Error; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn users, 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\nfunc UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {\n\tvar users []model.User\n\tvar modifiedUsernames []string\n\n\toldPathClean := path.Clean(oldPath)\n\n\tif len(usersOpt) > 0 {\n\t\tusers = usersOpt[0]\n\t} else {\n\t\tif err := db.Find(&users).Error; err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed to load users\")\n\t\t}\n\t}\n\n\tfor _, user := range users {\n\t\tbasePath := path.Clean(user.BasePath)\n\t\tupdated := false\n\n\t\tif basePath == oldPathClean {\n\t\t\tuser.BasePath = path.Clean(newPath)\n\t\t\tupdated = true\n\t\t} else if strings.HasPrefix(basePath, oldPathClean+\"/\") {\n\t\t\tuser.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])\n\t\t\tupdated = true\n\t\t}\n\n\t\tif updated {\n\t\t\tif err := UpdateUser(&user); err != nil {\n\t\t\t\treturn nil, errors.WithMessagef(err, \"failed to update user ID %d\", user.ID)\n\t\t\t}\n\t\t\tmodifiedUsernames = append(modifiedUsernames, user.Username)\n\t\t}\n\t}\n\n\treturn modifiedUsernames, nil\n}\n\nfunc CountUsersByRoleAndEnabledExclude(roleID uint, excludeUserID uint) (int64, error) {\n\tvar count int64\n\tjsonValue := fmt.Sprintf(\"[%d]\", roleID)\n\terr := db.Model(&model.User{}).\n\t\tWhere(\"disabled = ? AND id != ?\", false, excludeUserID).\n\t\tWhere(\"JSON_CONTAINS(role, ?)\", jsonValue).\n\t\tCount(&count).Error\n\treturn count, err\n}\n"
  },
  {
    "path": "internal/db/util.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/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/device/session.go",
    "content": "package device\n\nimport (\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\t\"gorm.io/gorm\"\n)\n\n// Handle verifies device sessions for a user and upserts current session.\nfunc Handle(userID uint, deviceKey, ua, ip string) error {\n\tttl := setting.GetInt(conf.DeviceSessionTTL, 86400)\n\tif ttl > 0 {\n\t\t_ = db.DeleteSessionsBefore(time.Now().Unix() - int64(ttl))\n\t}\n\n\tip = utils.MaskIP(ip)\n\n\tnow := time.Now().Unix()\n\tsess, err := db.GetSession(userID, deviceKey)\n\tif err == nil {\n\t\tif sess.Status == model.SessionInactive {\n\t\t\treturn errors.WithStack(errs.SessionInactive)\n\t\t}\n\t\tsess.Status = model.SessionActive\n\t\tsess.LastActive = now\n\t\tsess.UserAgent = ua\n\t\tsess.IP = ip\n\t\treturn db.UpsertSession(sess)\n\t}\n\tif err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn err\n\t}\n\n\tmax := setting.GetInt(conf.MaxDevices, 0)\n\tif max > 0 {\n\t\tcount, err := db.CountActiveSessionsByUser(userID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif count >= int64(max) {\n\t\t\tpolicy := setting.GetStr(conf.DeviceEvictPolicy, \"deny\")\n\t\t\tif policy == \"evict_oldest\" {\n\t\t\t\tif oldest, err := db.GetOldestActiveSession(userID); err == nil {\n\t\t\t\t\tif err := db.MarkInactive(oldest.DeviceKey); 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} else {\n\t\t\t\treturn errors.WithStack(errs.TooManyDevices)\n\t\t\t}\n\t\t}\n\t}\n\n\ts := &model.Session{UserID: userID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}\n\treturn db.CreateSession(s)\n}\n\n// EnsureActiveOnLogin is used only in login flow:\n// - If session exists (even Inactive): reactivate and refresh fields.\n// - If not exists: apply max-devices policy, then create Active session.\nfunc EnsureActiveOnLogin(userID uint, deviceKey, ua, ip string) error {\n\tip = utils.MaskIP(ip)\n\tnow := time.Now().Unix()\n\n\tsess, err := db.GetSession(userID, deviceKey)\n\tif err == nil {\n\t\tif sess.Status == model.SessionInactive {\n\t\t\tmax := setting.GetInt(conf.MaxDevices, 0)\n\t\t\tif max > 0 {\n\t\t\t\tcount, err := db.CountActiveSessionsByUser(userID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif count >= int64(max) {\n\t\t\t\t\tpolicy := setting.GetStr(conf.DeviceEvictPolicy, \"deny\")\n\t\t\t\t\tif policy == \"evict_oldest\" {\n\t\t\t\t\t\tif oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {\n\t\t\t\t\t\t\tif err := db.MarkInactive(oldest.DeviceKey); err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn errors.WithStack(errs.TooManyDevices)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsess.Status = model.SessionActive\n\t\tsess.LastActive = now\n\t\tsess.UserAgent = ua\n\t\tsess.IP = ip\n\t\treturn db.UpsertSession(sess)\n\t}\n\tif err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn err\n\t}\n\n\tmax := setting.GetInt(conf.MaxDevices, 0)\n\tif max > 0 {\n\t\tcount, err := db.CountActiveSessionsByUser(userID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif count >= int64(max) {\n\t\t\tpolicy := setting.GetStr(conf.DeviceEvictPolicy, \"deny\")\n\t\t\tif policy == \"evict_oldest\" {\n\t\t\t\tif oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {\n\t\t\t\t\tif err := db.MarkInactive(oldest.DeviceKey); 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} else {\n\t\t\t\treturn errors.WithStack(errs.TooManyDevices)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn db.CreateSession(&model.Session{\n\t\tUserID:     userID,\n\t\tDeviceKey:  deviceKey,\n\t\tUserAgent:  ua,\n\t\tIP:         ip,\n\t\tLastActive: now,\n\t\tStatus:     model.SessionActive,\n\t})\n}\n\n// Refresh updates last_active for the session.\nfunc Refresh(userID uint, deviceKey string) {\n\t_ = db.UpdateSessionLastActive(userID, deviceKey, time.Now().Unix())\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\tOnlyLocal         bool   `json:\"only_local\"`\n\tOnlyProxy         bool   `json:\"only_proxy\"`\n\tNoCache           bool   `json:\"no_cache\"`\n\tNoUpload          bool   `json:\"no_upload\"`\n\tNeedMs            bool   `json:\"need_ms\"` // if need get message from user, such as validate code\n\tDefaultRoot       string `json:\"default_root\"`\n\tCheckStatus       bool   `json:\"-\"`\n\tAlert             string `json:\"alert\"` //info,success,warning,danger\n\tNoOverwriteUpload bool   `json:\"-\"`     // whether to support overwrite upload\n\tProxyRangeOption  bool   `json:\"-\"`\n}\n\nfunc (c Config) MustProxy() bool {\n\treturn c.OnlyProxy || c.OnlyLocal\n}\n"
  },
  {
    "path": "internal/driver/driver.go",
    "content": "package driver\n\nimport (\n\t\"context\"\n\n\t\"github.com/alist-org/alist/v3/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 consider using a recursive\n\t// mutex like `semaphore.Weighted` to limit the maximum number of upload threads, preventing excessive\n\t// memory usage caused by buffering 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\n//type WriteResult interface {\n//\tMkdirResult\n//\tMoveResult\n//\tRenameResult\n//\tCopyResult\n//\tPutResult\n//\tRemove\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 consider using a recursive\n\t// mutex like `semaphore.Weighted` to limit the maximum number of upload threads, preventing excessive\n\t// memory usage caused by buffering 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 Reference interface {\n\tInitReference(storage Driver) 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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"io\"\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}\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/device.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tTooManyDevices  = errors.New(\"too many active devices\")\n\tSessionInactive = errors.New(\"session inactive\")\n)\n"
  },
  {
    "path": "internal/errs/driver.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tEmptyToken = errors.New(\"empty token\")\n\tLinkIsDir  = errors.New(\"link is dir\")\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(\"access using relative path is not allowed\")\n\n\tMoveBetweenTwoStorages = errors.New(\"can't move files between two storages, try to copy\")\n\tUploadNotSupported     = errors.New(\"upload not supported\")\n\n\tMetaNotFound     = errors.New(\"meta not found\")\n\tStorageNotFound  = errors.New(\"storage not found\")\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\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 IsNotImplement(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\tNotFolder      = errors.New(\"not a folder\")\n\tNotFile        = errors.New(\"not a file\")\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\tInvalidName      = errors.New(\"invalid file name\")\n)\n"
  },
  {
    "path": "internal/errs/role.go",
    "content": "package errs\n\nimport \"errors\"\n\nvar (\n\tErrChangeDefaultRole = errors.New(\"cannot modify admin role\")\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/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\"mime\"\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xhofe/tache\"\n)\n\ntype ArchiveDownloadTask struct {\n\ttask.TaskExtension\n\tmodel.ArchiveDecompressArgs\n\tstatus       string\n\tSrcObjPath   string\n\tDstDirPath   string\n\tsrcStorage   driver.Driver\n\tdstStorage   driver.Driver\n\tSrcStorageMp string\n\tDstStorageMp string\n}\n\nfunc (t *ArchiveDownloadTask) GetName() string {\n\treturn fmt.Sprintf(\"decompress [%s](%s)[%s] to [%s](%s) with password <%s>\", t.SrcStorageMp, t.SrcObjPath,\n\t\tt.InnerPath, t.DstStorageMp, t.DstDirPath, t.Password)\n}\n\nfunc (t *ArchiveDownloadTask) GetStatus() string {\n\treturn t.status\n}\n\nfunc (t *ArchiveDownloadTask) Run() error {\n\tt.ReinitCtx()\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\tArchiveContentUploadTaskManager.Add(uploadTask)\n\treturn nil\n}\n\nfunc (t *ArchiveDownloadTask) RunWithoutPushUploadTask() (*ArchiveContentUploadTask, error) {\n\tvar err error\n\tif t.srcStorage == nil {\n\t\tt.srcStorage, err = op.GetStorageByMountPath(t.SrcStorageMp)\n\t}\n\tsrcObj, tool, ss, err := op.GetArchiveToolAndStream(t.Ctx(), t.srcStorage, t.SrcObjPath, model.LinkArgs{\n\t\tHeader: http.Header{},\n\t})\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\tvar total, cur int64 = 0, 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\tfor _, s := range ss {\n\t\t\tif s.GetFile() == nil {\n\t\t\t\t_, err = stream.CacheFullInTempFileAndUpdateProgress(s, func(p float64) {\n\t\t\t\t\tt.SetProgress((float64(cur) + float64(s.GetSize())*p/100.0) / float64(total))\n\t\t\t\t})\n\t\t\t}\n\t\t\tcur += s.GetSize()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tt.SetProgress(100.0)\n\t\tdecompressUp = func(_ float64) {}\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.GetCreator(),\n\t\t},\n\t\tObjName:      baseName,\n\t\tInPlace:      !t.PutIntoNewDir,\n\t\tFilePath:     dir,\n\t\tDstDirPath:   t.DstDirPath,\n\t\tdstStorage:   t.dstStorage,\n\t\tDstStorageMp: t.DstStorageMp,\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\tDstDirPath   string\n\tdstStorage   driver.Driver\n\tDstStorageMp string\n\tfinalized    bool\n}\n\nfunc (t *ArchiveContentUploadTask) GetName() string {\n\treturn fmt.Sprintf(\"upload %s to [%s](%s)\", t.ObjName, t.DstStorageMp, t.DstDirPath)\n}\n\nfunc (t *ArchiveContentUploadTask) GetStatus() string {\n\treturn t.status\n}\n\nfunc (t *ArchiveContentUploadTask) Run() error {\n\tt.ReinitCtx()\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\tArchiveContentUploadTaskManager.Add(nextTsk)\n\t\treturn nil\n\t})\n}\n\nfunc (t *ArchiveContentUploadTask) RunWithNextTaskCallback(f func(nextTsk *ArchiveContentUploadTask) error) error {\n\tvar err error\n\tif t.dstStorage == nil {\n\t\tt.dstStorage, err = op.GetStorageByMountPath(t.DstStorageMp)\n\t}\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\tnextDstPath := t.DstDirPath\n\t\tif !t.InPlace {\n\t\t\tnextDstPath = stdpath.Join(nextDstPath, t.ObjName)\n\t\t\terr = op.MakeDir(t.Ctx(), t.dstStorage, nextDstPath)\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\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.GetCreator(),\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\tDstDirPath:   nextDstPath,\n\t\t\t\tdstStorage:   t.dstStorage,\n\t\t\t\tDstStorageMp: t.DstStorageMp,\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\tt.SetTotalBytes(info.Size())\n\t\tfile, err := os.Open(t.FilePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\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:     mime.TypeByExtension(filepath.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(t.Ctx(), t.dstStorage, t.DstDirPath, fs, t.SetProgress, true)\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\tfor retry < 10000 {\n\t\tnewPath := stdpath.Join(conf.Conf.TempDir, prefix+strconv.FormatUint(uint64(rand.Uint32()), 10))\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\ttaskCreator, _ := ctx.Value(\"user\").(*model.User)\n\ttsk := &ArchiveDownloadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\n\t\t},\n\t\tArchiveDecompressArgs: args,\n\t\tsrcStorage:            srcStorage,\n\t\tdstStorage:            dstStorage,\n\t\tSrcObjPath:            srcObjActualPath,\n\t\tDstDirPath:            dstDirActualPath,\n\t\tSrcStorageMp:          srcStorage.GetStorage().MountPath,\n\t\tDstStorageMp:          dstStorage.GetStorage().MountPath,\n\t}\n\tif ctx.Value(conf.NoTaskKey) != nil {\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\tcallback = func(t *ArchiveContentUploadTask) error {\n\t\t\te := t.RunWithNextTaskCallback(callback)\n\t\t\tt.deleteSrcFile()\n\t\t\treturn e\n\t\t}\n\t\treturn nil, uploadTask.RunWithNextTaskCallback(callback)\n\t} else {\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.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/xhofe/tache\"\n)\n\ntype CopyTask struct {\n\ttask.TaskExtension\n\tStatus       string        `json:\"-\"` //don't save status to save space\n\tSrcObjPath   string        `json:\"src_path\"`\n\tDstDirPath   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 *CopyTask) GetName() string {\n\treturn fmt.Sprintf(\"copy [%s](%s) to [%s](%s)\", t.SrcStorageMp, t.SrcObjPath, t.DstStorageMp, t.DstDirPath)\n}\n\nfunc (t *CopyTask) GetStatus() string {\n\treturn t.Status\n}\n\nfunc (t *CopyTask) Run() error {\n\tt.ReinitCtx()\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\tvar err error\n\tif t.srcStorage == nil {\n\t\tt.srcStorage, err = op.GetStorageByMountPath(t.SrcStorageMp)\n\t}\n\tif t.dstStorage == nil {\n\t\tt.dstStorage, err = op.GetStorageByMountPath(t.DstStorageMp)\n\t}\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get storage\")\n\t}\n\treturn copyBetween2Storages(t, t.srcStorage, t.dstStorage, t.SrcObjPath, t.DstDirPath)\n}\n\nvar CopyTaskManager *tache.Manager[*CopyTask]\n\n// Copy if in the same storage, call move method\n// if not, add copy task\nfunc _copy(ctx context.Context, srcObjPath, dstDirPath string, 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\t// copy if in the same storage, just call driver.Copy\n\tif srcStorage.GetStorage() == dstStorage.GetStorage() {\n\t\terr = op.Copy(ctx, srcStorage, srcObjActualPath, dstDirActualPath, lazyCache...)\n\t\tif !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif ctx.Value(conf.NoTaskKey) != nil {\n\t\tsrcObj, err := op.Get(ctx, srcStorage, srcObjActualPath)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessagef(err, \"failed get src [%s] file\", srcObjPath)\n\t\t}\n\t\tif !srcObj.IsDir() {\n\t\t\t// copy file directly\n\t\t\tlink, _, err := op.Link(ctx, srcStorage, srcObjActualPath, model.LinkArgs{\n\t\t\t\tHeader: http.Header{},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.WithMessagef(err, \"failed get [%s] link\", srcObjPath)\n\t\t\t}\n\t\t\tfs := stream.FileStream{\n\t\t\t\tObj: srcObj,\n\t\t\t\tCtx: ctx,\n\t\t\t}\n\t\t\t// any link provided is seekable\n\t\t\tss, err := stream.NewSeekableStream(fs, link)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.WithMessagef(err, \"failed get [%s] stream\", srcObjPath)\n\t\t\t}\n\t\t\treturn nil, op.Put(ctx, dstStorage, dstDirActualPath, ss, nil, false)\n\t\t}\n\t}\n\t// not in the same storage\n\ttaskCreator, _ := ctx.Value(\"user\").(*model.User)\n\tt := &CopyTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\n\t\t},\n\t\tsrcStorage:   srcStorage,\n\t\tdstStorage:   dstStorage,\n\t\tSrcObjPath:   srcObjActualPath,\n\t\tDstDirPath:   dstDirActualPath,\n\t\tSrcStorageMp: srcStorage.GetStorage().MountPath,\n\t\tDstStorageMp: dstStorage.GetStorage().MountPath,\n\t}\n\tCopyTaskManager.Add(t)\n\treturn t, nil\n}\n\nfunc copyBetween2Storages(t *CopyTask, srcStorage, dstStorage driver.Driver, srcObjPath, dstDirPath string) error {\n\tt.Status = \"getting src object\"\n\tsrcObj, err := op.Get(t.Ctx(), srcStorage, srcObjPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", srcObjPath)\n\t}\n\tif srcObj.IsDir() {\n\t\tt.Status = \"src object is dir, listing objs\"\n\t\tobjs, err := op.List(t.Ctx(), srcStorage, srcObjPath, model.ListArgs{})\n\t\tif err != nil {\n\t\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", srcObjPath)\n\t\t}\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(srcObjPath, obj.GetName())\n\t\t\tdstObjPath := stdpath.Join(dstDirPath, srcObj.GetName())\n\t\t\tCopyTaskManager.Add(&CopyTask{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: t.GetCreator(),\n\t\t\t\t},\n\t\t\t\tsrcStorage:   srcStorage,\n\t\t\t\tdstStorage:   dstStorage,\n\t\t\t\tSrcObjPath:   srcObjPath,\n\t\t\t\tDstDirPath:   dstObjPath,\n\t\t\t\tSrcStorageMp: srcStorage.GetStorage().MountPath,\n\t\t\t\tDstStorageMp: dstStorage.GetStorage().MountPath,\n\t\t\t})\n\t\t}\n\t\tt.Status = \"src object is dir, added all copy tasks of objs\"\n\t\treturn nil\n\t}\n\treturn copyFileBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstDirPath)\n}\n\nfunc copyFileBetween2Storages(tsk *CopyTask, srcStorage, dstStorage driver.Driver, srcFilePath, dstDirPath string) error {\n\tsrcFile, err := op.Get(tsk.Ctx(), srcStorage, srcFilePath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", srcFilePath)\n\t}\n\ttsk.SetTotalBytes(srcFile.GetSize())\n\tlink, _, err := op.Link(tsk.Ctx(), srcStorage, srcFilePath, model.LinkArgs{\n\t\tHeader: http.Header{},\n\t})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] link\", srcFilePath)\n\t}\n\tfs := stream.FileStream{\n\t\tObj: srcFile,\n\t\tCtx: tsk.Ctx(),\n\t}\n\t// any link provided is seekable\n\tss, err := stream.NewSeekableStream(fs, link)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] stream\", srcFilePath)\n\t}\n\treturn op.Put(tsk.Ctx(), dstStorage, dstDirPath, ss, tsk.SetProgress, true)\n}\n"
  },
  {
    "path": "internal/fs/fs.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"io\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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}\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}\n\nfunc Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {\n\tres, err := get(ctx, path)\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, lazyCache ...bool) error {\n\terr := makeDir(ctx, path, lazyCache...)\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, lazyCache ...bool) error {\n\terr := move(ctx, srcPath, dstDirPath, lazyCache...)\n\tif err != nil {\n\t\tlog.Errorf(\"failed move %s to %s: %+v\", srcPath, dstDirPath, err)\n\t}\n\treturn err\n}\n\nfunc Copy(ctx context.Context, srcObjPath, dstDirPath string, lazyCache ...bool) (task.TaskExtensionInfo, error) {\n\tres, err := _copy(ctx, srcObjPath, dstDirPath, lazyCache...)\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 Rename(ctx context.Context, srcPath, dstName string, lazyCache ...bool) error {\n\terr := rename(ctx, srcPath, dstName, lazyCache...)\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, lazyCache ...bool) error {\n\terr := putDirectly(ctx, dstDirPath, file, lazyCache...)\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 remove %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"
  },
  {
    "path": "internal/fs/get.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc get(ctx context.Context, path string) (model.Obj, error) {\n\tpath = utils.FixAndCleanPath(path)\n\t// maybe a virtual file\n\tif path != \"/\" {\n\t\tvirtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(path))\n\t\tfor _, f := range virtualFiles {\n\t\t\tif f.GetName() == stdpath.Base(path) {\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\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\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\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\tif c, ok := ctx.(*gin.Context); ok {\n\t\t\tl.URL = common.GetApiUrl(c.Request) + l.URL\n\t\t}\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\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(\"meta\").(*model.Meta)\n\tuser, _ := ctx.Value(\"user\").(*model.User)\n\tvirtualFiles := op.GetStorageVirtualFilesByPath(path)\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})\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 user is nil, don't hide\n\tif user == nil {\n\t\treturn false\n\t}\n\tperm := common.MergeRolePermissions(user, path)\n\t// if user has see-hides permission, don't hide\n\tif common.HasPermission(perm, common.PermSeeHides) {\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\t\"encoding/json\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/drivers/s3\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc makeDir(ctx context.Context, path string, lazyCache ...bool) 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, lazyCache...)\n}\n\nfunc move(ctx context.Context, srcPath, dstDirPath string, lazyCache ...bool) error {\n\tsrcStorage, srcActualPath, err := op.GetStorageAndActualPath(srcPath)\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\tif srcStorage.GetStorage() != dstStorage.GetStorage() {\n\t\treturn errors.WithStack(errs.MoveBetweenTwoStorages)\n\t}\n\treturn op.Move(ctx, srcStorage, srcActualPath, dstDirActualPath, lazyCache...)\n}\n\nfunc rename(ctx context.Context, srcPath, dstName string, lazyCache ...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\treturn op.Rename(ctx, storage, srcActualPath, dstName, lazyCache...)\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\toriginalPath := args.Path\n\n\tif _, ok := storage.(*s3.S3); ok {\n\t\tmethod := strings.ToLower(strings.TrimSpace(args.Method))\n\t\tif method == s3.OtherMethodArchive || method == s3.OtherMethodThaw {\n\t\t\tif S3TransitionTaskManager == nil {\n\t\t\t\treturn nil, errors.New(\"s3 transition task manager is not initialized\")\n\t\t\t}\n\t\t\tvar payload json.RawMessage\n\t\t\tif args.Data != nil {\n\t\t\t\traw, err := json.Marshal(args.Data)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.WithMessage(err, \"failed to encode request payload\")\n\t\t\t\t}\n\t\t\t\tpayload = raw\n\t\t\t}\n\t\t\ttaskCreator, _ := ctx.Value(\"user\").(*model.User)\n\t\t\ttsk := &S3TransitionTask{\n\t\t\t\tTaskExtension:    task.TaskExtension{Creator: taskCreator},\n\t\t\t\tstatus:           \"queued\",\n\t\t\t\tStorageMountPath: storage.GetStorage().MountPath,\n\t\t\t\tObjectPath:       actualPath,\n\t\t\t\tDisplayPath:      originalPath,\n\t\t\t\tObjectName:       stdpath.Base(actualPath),\n\t\t\t\tTransition:       method,\n\t\t\t\tPayload:          payload,\n\t\t\t}\n\t\t\tS3TransitionTaskManager.Add(tsk)\n\t\t\treturn map[string]string{\"task_id\": tsk.GetID()}, nil\n\t\t}\n\t}\n\n\targs.Path = actualPath\n\treturn op.Other(ctx, storage, args)\n}\n"
  },
  {
    "path": "internal/fs/put.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/xhofe/tache\"\n\t\"time\"\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(t.Ctx(), t.storage, t.dstDirActualPath, t.file, t.SetProgress, true)\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.CacheFullInTempFile()\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(\"user\").(*model.User) // taskCreator is nil when convert failed\n\tt := &UploadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\n\t\t},\n\t\tstorage:          storage,\n\t\tdstDirActualPath: dstDirActualPath,\n\t\tfile:             file,\n\t}\n\tt.SetTotalBytes(file.GetSize())\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, lazyCache ...bool) error {\n\tstorage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)\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\treturn op.Put(ctx, storage, dstDirActualPath, file, nil, lazyCache...)\n}\n"
  },
  {
    "path": "internal/fs/s3_transition.go",
    "content": "package fs\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/drivers/s3\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/xhofe/tache\"\n)\n\nconst s3TransitionPollInterval = 15 * time.Second\n\n// S3TransitionTask represents an asynchronous S3 archive/thaw request that is\n// tracked via the task manager so that clients can monitor the progress of the\n// operation.\ntype S3TransitionTask struct {\n\ttask.TaskExtension\n\tstatus string\n\n\tStorageMountPath string          `json:\"storage_mount_path\"`\n\tObjectPath       string          `json:\"object_path\"`\n\tDisplayPath      string          `json:\"display_path\"`\n\tObjectName       string          `json:\"object_name\"`\n\tTransition       string          `json:\"transition\"`\n\tPayload          json.RawMessage `json:\"payload,omitempty\"`\n\n\tTargetStorageClass string `json:\"target_storage_class,omitempty\"`\n\tRequestID          string `json:\"request_id,omitempty\"`\n\tVersionID          string `json:\"version_id,omitempty\"`\n\n\tstorage driver.Driver `json:\"-\"`\n}\n\n// S3TransitionTaskManager holds asynchronous S3 archive/thaw tasks.\nvar S3TransitionTaskManager *tache.Manager[*S3TransitionTask]\n\nvar _ task.TaskExtensionInfo = (*S3TransitionTask)(nil)\n\nfunc (t *S3TransitionTask) GetName() string {\n\taction := strings.ToLower(t.Transition)\n\tif action == \"\" {\n\t\taction = \"transition\"\n\t}\n\tdisplay := t.DisplayPath\n\tif display == \"\" {\n\t\tdisplay = t.ObjectPath\n\t}\n\tif display == \"\" {\n\t\tdisplay = t.ObjectName\n\t}\n\treturn fmt.Sprintf(\"s3 %s %s\", action, display)\n}\n\nfunc (t *S3TransitionTask) GetStatus() string {\n\treturn t.status\n}\n\nfunc (t *S3TransitionTask) Run() error {\n\tt.ReinitCtx()\n\tt.ClearEndTime()\n\tstart := time.Now()\n\tt.SetStartTime(start)\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\n\tif err := t.ensureStorage(); err != nil {\n\t\tt.status = fmt.Sprintf(\"locate storage failed: %v\", err)\n\t\treturn err\n\t}\n\n\tpayload, err := t.decodePayload()\n\tif err != nil {\n\t\tt.status = fmt.Sprintf(\"decode payload failed: %v\", err)\n\t\treturn err\n\t}\n\n\tmethod := strings.ToLower(strings.TrimSpace(t.Transition))\n\tswitch method {\n\tcase s3.OtherMethodArchive:\n\t\tt.status = \"submitting archive request\"\n\t\tt.SetProgress(0)\n\t\tresp, err := op.Other(t.Ctx(), t.storage, model.FsOtherArgs{\n\t\t\tPath:   t.ObjectPath,\n\t\t\tMethod: s3.OtherMethodArchive,\n\t\t\tData:   payload,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.status = fmt.Sprintf(\"archive request failed: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tarchiveResp, ok := toArchiveResponse(resp)\n\t\tif ok {\n\t\t\tif t.TargetStorageClass == \"\" {\n\t\t\t\tt.TargetStorageClass = archiveResp.StorageClass\n\t\t\t}\n\t\t\tt.RequestID = archiveResp.RequestID\n\t\t\tt.VersionID = archiveResp.VersionID\n\t\t\tif archiveResp.StorageClass != \"\" {\n\t\t\t\tt.status = fmt.Sprintf(\"archive requested, waiting for %s\", archiveResp.StorageClass)\n\t\t\t} else {\n\t\t\t\tt.status = \"archive requested\"\n\t\t\t}\n\t\t} else if sc := t.extractTargetStorageClass(); sc != \"\" {\n\t\t\tt.TargetStorageClass = sc\n\t\t\tt.status = fmt.Sprintf(\"archive requested, waiting for %s\", sc)\n\t\t} else {\n\t\t\tt.status = \"archive requested\"\n\t\t}\n\t\tif t.TargetStorageClass != \"\" {\n\t\t\tt.TargetStorageClass = s3.NormalizeStorageClass(t.TargetStorageClass)\n\t\t}\n\t\tt.SetProgress(25)\n\t\treturn t.waitForArchive()\n\tcase s3.OtherMethodThaw:\n\t\tt.status = \"submitting thaw request\"\n\t\tt.SetProgress(0)\n\t\tresp, err := op.Other(t.Ctx(), t.storage, model.FsOtherArgs{\n\t\t\tPath:   t.ObjectPath,\n\t\t\tMethod: s3.OtherMethodThaw,\n\t\t\tData:   payload,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.status = fmt.Sprintf(\"thaw request failed: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tthawResp, ok := toThawResponse(resp)\n\t\tif ok {\n\t\t\tt.RequestID = thawResp.RequestID\n\t\t\tif thawResp.Status != nil && !thawResp.Status.Ongoing {\n\t\t\t\tt.SetProgress(100)\n\t\t\t\tt.status = thawCompletionMessage(thawResp.Status)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tt.status = \"thaw requested\"\n\t\tt.SetProgress(25)\n\t\treturn t.waitForThaw()\n\tdefault:\n\t\treturn errors.Errorf(\"unsupported transition method: %s\", t.Transition)\n\t}\n}\n\nfunc (t *S3TransitionTask) ensureStorage() error {\n\tif t.storage != nil {\n\t\treturn nil\n\t}\n\tstorage, err := op.GetStorageByMountPath(t.StorageMountPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tt.storage = storage\n\treturn nil\n}\n\nfunc (t *S3TransitionTask) decodePayload() (interface{}, error) {\n\tif len(t.Payload) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar payload interface{}\n\tif err := json.Unmarshal(t.Payload, &payload); err != nil {\n\t\treturn nil, err\n\t}\n\treturn payload, nil\n}\n\nfunc (t *S3TransitionTask) extractTargetStorageClass() string {\n\tif len(t.Payload) == 0 {\n\t\treturn \"\"\n\t}\n\tvar req s3.ArchiveRequest\n\tif err := json.Unmarshal(t.Payload, &req); err != nil {\n\t\treturn \"\"\n\t}\n\treturn s3.NormalizeStorageClass(req.StorageClass)\n}\n\nfunc (t *S3TransitionTask) waitForArchive() error {\n\tticker := time.NewTicker(s3TransitionPollInterval)\n\tdefer ticker.Stop()\n\n\tctx := t.Ctx()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tt.status = \"archive canceled\"\n\t\t\treturn ctx.Err()\n\t\tcase <-ticker.C:\n\t\t\tresp, err := op.Other(ctx, t.storage, model.FsOtherArgs{\n\t\t\t\tPath:   t.ObjectPath,\n\t\t\t\tMethod: s3.OtherMethodArchiveStatus,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.status = fmt.Sprintf(\"archive status error: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tarchiveResp, ok := toArchiveResponse(resp)\n\t\t\tif !ok {\n\t\t\t\tt.status = fmt.Sprintf(\"unexpected archive status response: %T\", resp)\n\t\t\t\treturn errors.Errorf(\"unexpected archive status response: %T\", resp)\n\t\t\t}\n\t\t\tcurrentClass := strings.TrimSpace(archiveResp.StorageClass)\n\t\t\ttarget := strings.TrimSpace(t.TargetStorageClass)\n\t\t\tif target == \"\" {\n\t\t\t\ttarget = currentClass\n\t\t\t\tt.TargetStorageClass = currentClass\n\t\t\t}\n\t\t\tif currentClass == \"\" {\n\t\t\t\tt.status = \"waiting for storage class update\"\n\t\t\t\tt.SetProgress(50)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.EqualFold(currentClass, target) {\n\t\t\t\tt.SetProgress(100)\n\t\t\t\tt.status = fmt.Sprintf(\"archive complete (%s)\", currentClass)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tt.status = fmt.Sprintf(\"storage class %s (target %s)\", currentClass, target)\n\t\t\tt.SetProgress(75)\n\t\t}\n\t}\n}\n\nfunc (t *S3TransitionTask) waitForThaw() error {\n\tticker := time.NewTicker(s3TransitionPollInterval)\n\tdefer ticker.Stop()\n\n\tctx := t.Ctx()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tt.status = \"thaw canceled\"\n\t\t\treturn ctx.Err()\n\t\tcase <-ticker.C:\n\t\t\tresp, err := op.Other(ctx, t.storage, model.FsOtherArgs{\n\t\t\t\tPath:   t.ObjectPath,\n\t\t\t\tMethod: s3.OtherMethodThawStatus,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.status = fmt.Sprintf(\"thaw status error: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tthawResp, ok := toThawResponse(resp)\n\t\t\tif !ok {\n\t\t\t\tt.status = fmt.Sprintf(\"unexpected thaw status response: %T\", resp)\n\t\t\t\treturn errors.Errorf(\"unexpected thaw status response: %T\", resp)\n\t\t\t}\n\t\t\tstatus := thawResp.Status\n\t\t\tif status == nil {\n\t\t\t\tt.status = \"waiting for thaw status\"\n\t\t\t\tt.SetProgress(50)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif status.Ongoing {\n\t\t\t\tt.status = fmt.Sprintf(\"thaw in progress (%s)\", status.Raw)\n\t\t\t\tt.SetProgress(75)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tt.SetProgress(100)\n\t\t\tt.status = thawCompletionMessage(status)\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc thawCompletionMessage(status *s3.RestoreStatus) string {\n\tif status == nil {\n\t\treturn \"thaw complete\"\n\t}\n\tif status.Expiry != \"\" {\n\t\treturn fmt.Sprintf(\"thaw complete, expires %s\", status.Expiry)\n\t}\n\treturn \"thaw complete\"\n}\n\nfunc toArchiveResponse(v interface{}) (s3.ArchiveResponse, bool) {\n\tswitch resp := v.(type) {\n\tcase s3.ArchiveResponse:\n\t\treturn resp, true\n\tcase *s3.ArchiveResponse:\n\t\tif resp != nil {\n\t\t\treturn *resp, true\n\t\t}\n\t}\n\treturn s3.ArchiveResponse{}, false\n}\n\nfunc toThawResponse(v interface{}) (s3.ThawResponse, bool) {\n\tswitch resp := v.(type) {\n\tcase s3.ThawResponse:\n\t\treturn resp, true\n\tcase *s3.ThawResponse:\n\t\tif resp != nil {\n\t\t\treturn *resp, true\n\t\t}\n\t}\n\treturn s3.ThawResponse{}, false\n}\n\n// Ensure compatibility with persistence when tasks are restored.\nfunc (t *S3TransitionTask) OnRestore() {\n\t// The storage handle is not persisted intentionally; it will be lazily\n\t// re-fetched on the next Run invocation.\n\tt.storage = nil\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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, \"meta\", 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/alist-org/alist/v3/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/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype ListArgs struct {\n\tReqPath           string\n\tS3ShowPlaceholder bool\n\tRefresh           bool\n}\n\ntype LinkArgs struct {\n\tIP       string\n\tHeader   http.Header\n\tType     string\n\tHttpReq  *http.Request\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\tRangeReadCloser RangeReadCloserIF `json:\"-\"`      // recommended way if can't use URL\n\tMFile           File              `json:\"-\"`      // best for local,smb... file system, which exposes MFile\n\n\tExpiration *time.Duration // local cache expire Duration\n\tIPCacheKey bool           `json:\"-\"` // add ip to cache key\n\n\t//for accelerating request, use multi-thread downloading\n\tConcurrency int `json:\"concurrency\"`\n\tPartSize    int `json:\"part_size\"`\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}\n\ntype RangeReadCloserIF interface {\n\tRangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error)\n\tutils.ClosersIF\n}\n\nvar _ RangeReadCloserIF = (*RangeReadCloser)(nil)\n\ntype RangeReadCloser struct {\n\tRangeReader RangeReaderFunc\n\tutils.Closers\n}\n\nfunc (r *RangeReadCloser) RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\trc, err := r.RangeReader(ctx, httpRange)\n\tr.Closers.Add(rc)\n\treturn rc, err\n}\n\n// type WriterFunc func(w io.Writer) error\ntype RangeReaderFunc func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error)\n"
  },
  {
    "path": "internal/model/file.go",
    "content": "package model\n\nimport \"io\"\n\n// File is basic file level accessing interface\ntype File interface {\n\tio.Reader\n\tio.ReaderAt\n\tio.Seeker\n\tio.Closer\n}\n\ntype NopMFileIF interface {\n\tio.Reader\n\tio.ReaderAt\n\tio.Seeker\n}\ntype NopMFile struct {\n\tNopMFileIF\n}\n\nfunc (NopMFile) Close() error { return nil }\nfunc NewNopMFile(r NopMFileIF) File {\n\treturn NopMFile{r}\n}\n"
  },
  {
    "path": "internal/model/label.go",
    "content": "package model\n\nimport \"time\"\n\ntype Label struct {\n\tID          uint      `json:\"id\" gorm:\"primaryKey\"` // unique key\n\tType        int       `json:\"type\"`                 // use to type\n\tName        string    `json:\"name\"`                 // use to name\n\tDescription string    `json:\"description\"`          // use to description\n\tBgColor     string    `json:\"bg_color\"`             // use to bg_color\n\tCreateTime  time.Time `json:\"create_time\"`\n}\n"
  },
  {
    "path": "internal/model/label_file_binding.go",
    "content": "package model\n\nimport \"time\"\n\ntype LabelFileBinding struct {\n\tID         uint      `json:\"id\" gorm:\"primaryKey\"` // unique key\n\tUserId     uint      `json:\"user_id\"`              // use to user_id\n\tLabelId    uint      `json:\"label_id\"`             // use to label_id\n\tFileName   string    `json:\"file_name\"`            // use to file_name\n\tCreateTime time.Time `json:\"create_time\"`\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\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/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 StorageClassProvider interface {\n\tStorageClass() string\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\tio.Closer\n\tObj\n\tGetMimetype() string\n\t//SetReader(io.Reader)\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 CacheFullInTempFile 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\tCacheFullInTempFile() (File, error)\n\tSetTmpFile(r *os.File)\n\tGetFile() File\n}\n\ntype UpdateProgress func(percentage float64)\n\n// Reference implementation from OpenListTeam:\n// https://github.com/OpenListTeam/OpenList/blob/a703b736c9346c483bae56905a39bc07bf781cff/internal/model/obj.go#L58\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\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 := 0; i < len(objs); i++ {\n\t\tobjs[i] = &ObjWrapName{Name: utils.MappingName(objs[i].GetName()), Obj: objs[i]}\n\t}\n}\n\nfunc WrapObjStorageClass(obj Obj, storageClass string) Obj {\n\tif storageClass == \"\" {\n\t\treturn obj\n\t}\n\treturn &ObjWrapStorageClass{Obj: obj, storageClass: storageClass}\n}\n\nfunc UnwrapObj(obj Obj) Obj {\n\tif unwrap, ok := obj.(ObjUnwrap); ok {\n\t\tobj = unwrap.Unwrap()\n\t}\n\treturn obj\n}\n\nfunc GetThumb(obj Obj) (thumb string, ok bool) {\n\tif obj, ok := obj.(Thumb); ok {\n\t\treturn obj.Thumb(), true\n\t}\n\tif unwrap, ok := obj.(ObjUnwrap); ok {\n\t\treturn GetThumb(unwrap.Unwrap())\n\t}\n\treturn thumb, false\n}\n\nfunc GetUrl(obj Obj) (url string, ok bool) {\n\tif obj, ok := obj.(URL); ok {\n\t\treturn obj.URL(), true\n\t}\n\tif unwrap, ok := obj.(ObjUnwrap); ok {\n\t\treturn GetUrl(unwrap.Unwrap())\n\t}\n\treturn url, false\n}\n\nfunc GetStorageClass(obj Obj) (string, bool) {\n\tif provider, ok := obj.(StorageClassProvider); ok {\n\t\tvalue := provider.StorageClass()\n\t\tif value == \"\" {\n\t\t\treturn \"\", false\n\t\t}\n\t\treturn value, true\n\t}\n\tif unwrap, ok := obj.(ObjUnwrap); ok {\n\t\treturn GetStorageClass(unwrap.Unwrap())\n\t}\n\treturn \"\", false\n}\n\nfunc GetRawObject(obj Obj) *Object {\n\tswitch v := obj.(type) {\n\tcase *ObjThumbURL:\n\t\treturn &v.Object\n\tcase *ObjThumb:\n\t\treturn &v.Object\n\tcase *ObjectURL:\n\t\treturn &v.Object\n\tcase *Object:\n\t\treturn v\n\t}\n\treturn nil\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"
  },
  {
    "path": "internal/model/obj_file.go",
    "content": "package model\n\nimport \"time\"\n\ntype ObjFile struct {\n\tId          string    `json:\"id\"`\n\tUserId      uint      `json:\"user_id\"`\n\tPath        string    `json:\"path\"`\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}\n"
  },
  {
    "path": "internal/model/object.go",
    "content": "package model\n\nimport (\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\ntype ObjWrapName struct {\n\tName string\n\tObj\n}\n\ntype ObjWrapStorageClass struct {\n\tstorageClass 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\nfunc (o *ObjWrapStorageClass) Unwrap() Obj {\n\treturn o.Obj\n}\n\nfunc (o *ObjWrapStorageClass) StorageClass() string {\n\treturn o.storageClass\n}\n\nfunc (o *ObjWrapStorageClass) SetPath(path string) {\n\tif setter, ok := o.Obj.(SetPath); ok {\n\t\tsetter.SetPath(path)\n\t}\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}\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\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"
  },
  {
    "path": "internal/model/paths.go",
    "content": "package model\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype Paths []string\n\nfunc (p Paths) Value() (driver.Value, error) {\n\treturn json.Marshal([]string(p))\n}\n\nfunc (p *Paths) Scan(value interface{}) error {\n\tswitch v := value.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(v, (*[]string)(p))\n\tcase string:\n\t\treturn json.Unmarshal([]byte(v), (*[]string)(p))\n\tcase nil:\n\t\t*p = nil\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"cannot scan %T\", value)\n\t}\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/role.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\n\t\"gorm.io/gorm\"\n)\n\n// PermissionEntry defines permission bitmask for a specific path.\ntype PermissionEntry struct {\n\tPath       string `json:\"path\"`       // path prefix, e.g. \"/admin\"\n\tPermission int32  `json:\"permission\"` // bitmask permissions\n}\n\n// Role represents a permission template which can be bound to users.\ntype Role struct {\n\tID          uint   `json:\"id\" gorm:\"primaryKey\"`\n\tName        string `json:\"name\" gorm:\"unique\" binding:\"required\"`\n\tDescription string `json:\"description\"`\n\tDefault     bool   `json:\"default\" gorm:\"default:false\"`\n\t// PermissionScopes stores structured permission list and is ignored by gorm.\n\tPermissionScopes []PermissionEntry `json:\"permission_scopes\" gorm:\"-\"`\n\t// RawPermission is the JSON representation of PermissionScopes stored in DB.\n\tRawPermission string `json:\"-\" gorm:\"type:text\"`\n}\n\n// BeforeSave GORM hook serializes PermissionScopes into RawPermission.\nfunc (r *Role) BeforeSave(tx *gorm.DB) error {\n\tif len(r.PermissionScopes) == 0 {\n\t\tr.RawPermission = \"\"\n\t\treturn nil\n\t}\n\tbs, err := json.Marshal(r.PermissionScopes)\n\tif err != nil {\n\t\treturn err\n\t}\n\tr.RawPermission = string(bs)\n\treturn nil\n}\n\n// AfterFind GORM hook deserializes RawPermission into PermissionScopes.\nfunc (r *Role) AfterFind(tx *gorm.DB) error {\n\tif r.RawPermission == \"\" {\n\t\tr.PermissionScopes = nil\n\t\treturn nil\n\t}\n\tvar scopes []PermissionEntry\n\tif err := json.Unmarshal([]byte(r.RawPermission), &scopes); err != nil {\n\t\treturn err\n\t}\n\tr.PermissionScopes = scopes\n\treturn nil\n}\n"
  },
  {
    "path": "internal/model/roles.go",
    "content": "package model\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype Roles []int\n\nfunc (r Roles) Value() (driver.Value, error) {\n\treturn json.Marshal([]int(r))\n}\n\nfunc (r *Roles) Scan(value interface{}) error {\n\tswitch v := value.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(v, (*[]int)(r))\n\tcase string:\n\t\treturn json.Unmarshal([]byte(v), (*[]int)(r))\n\tcase nil:\n\t\t*r = nil\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"cannot scan %T\", value)\n\t}\n}\n\nfunc (r Roles) Contains(role int) bool {\n\tfor _, v := range r {\n\t\tif v == role {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\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/session.go",
    "content": "package model\n\n// Session represents a device session of a user.\ntype Session struct {\n\tUserID     uint   `json:\"user_id\" gorm:\"index\"`\n\tDeviceKey  string `json:\"device_key\" gorm:\"primaryKey;size:64\"`\n\tUserAgent  string `json:\"user_agent\" gorm:\"size:255\"`\n\tIP         string `json:\"ip\" gorm:\"size:64\"`\n\tLastActive int64  `json:\"last_active\"`\n\tStatus     int    `json:\"status\"`\n}\n\nconst (\n\tSessionActive = iota\n\tSessionInactive\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\tPreDefault 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/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\"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\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\tDownProxySign bool   `json:\"down_proxy_sign\" gorm:\"default:true\"`\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) WebdavProxy() bool {\n\treturn p.WebdavPolicy == \"use_proxy_url\"\n}\n\nfunc (p Proxy) WebdavNative() bool {\n\treturn !p.Webdav302() && !p.WebdavProxy()\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/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\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\tNEWGENERAL\n)\n\nconst StaticHashSalt = \"https://github.com/alist-org/alist\"\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        Roles  `json:\"role\" gorm:\"type:text\"`                     // user's roles\n\tRolesDetail []Role `json:\"-\" gorm:\"-\"`\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: check path limit\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}\n\nfunc (u *User) IsGuest() bool {\n\treturn u.Role.Contains(GUEST)\n}\n\nfunc (u *User) IsAdmin() bool {\n\treturn u.Role.Contains(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 (u *User) CanSeeHides() bool {\n\treturn u.Permission&1 == 1\n}\n\nfunc (u *User) CanAccessWithoutPassword() bool {\n\treturn (u.Permission>>1)&1 == 1\n}\n\nfunc (u *User) CanAddOfflineDownloadTasks() bool {\n\treturn (u.Permission>>2)&1 == 1\n}\n\nfunc (u *User) CanWrite() bool {\n\treturn (u.Permission>>3)&1 == 1\n}\n\nfunc (u *User) CanRename() bool {\n\treturn (u.Permission>>4)&1 == 1\n}\n\nfunc (u *User) CanMove() bool {\n\treturn (u.Permission>>5)&1 == 1\n}\n\nfunc (u *User) CanCopy() bool {\n\treturn (u.Permission>>6)&1 == 1\n}\n\nfunc (u *User) CanRemove() bool {\n\treturn (u.Permission>>7)&1 == 1\n}\n\nfunc (u *User) CanWebdavRead() bool {\n\treturn (u.Permission>>8)&1 == 1\n}\n\nfunc (u *User) CanWebdavManage() bool {\n\treturn (u.Permission>>9)&1 == 1\n}\n\nfunc (u *User) CanFTPAccess() bool {\n\treturn (u.Permission>>10)&1 == 1\n}\n\nfunc (u *User) CanFTPManage() bool {\n\treturn (u.Permission>>11)&1 == 1\n}\n\nfunc (u *User) CanReadArchives() bool {\n\treturn (u.Permission>>12)&1 == 1\n}\n\nfunc (u *User) CanDecompress() bool {\n\treturn (u.Permission>>13)&1 == 1\n}\n\nfunc (u *User) CheckPathLimit() bool {\n\treturn (u.Permission>>14)&1 == 1\n}\n\nfunc (u *User) JoinPath(reqPath string) (string, error) {\n\tif reqPath == \"/\" {\n\t\treturn utils.FixAndCleanPath(u.BasePath), nil\n\t}\n\tpath, err := utils.JoinBasePath(u.BasePath, reqPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif path != \"/\" && u.CheckPathLimit() {\n\t\tbasePaths := GetAllBasePathsFromRoles(u)\n\t\tmatch := false\n\t\tfor _, base := range basePaths {\n\t\t\tif utils.IsSubPath(base, path) {\n\t\t\t\tmatch = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !match {\n\t\t\treturn \"\", errs.PermissionDenied\n\t\t}\n\t}\n\n\treturn path, nil\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://alistgo.com/logo.svg\"\n}\n\n// FetchRole is used to load role details by id. It should be set by the op package\n// to avoid an import cycle between model and op.\nvar FetchRole func(uint) (*Role, error)\n\n// GetAllBasePathsFromRoles returns all permission paths from user's roles\nfunc GetAllBasePathsFromRoles(u *User) []string {\n\tbasePaths := make([]string, 0)\n\tseen := make(map[string]struct{})\n\n\tfor _, rid := range u.Role {\n\t\tif FetchRole == nil {\n\t\t\tcontinue\n\t\t}\n\t\trole, err := FetchRole(uint(rid))\n\t\tif err != nil || role == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range role.PermissionScopes {\n\t\t\tif entry.Path == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := seen[entry.Path]; !ok {\n\t\t\t\tbasePaths = append(basePaths, entry.Path)\n\t\t\t\tseen[entry.Path] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\treturn basePaths\n}\n"
  },
  {
    "path": "internal/net/request.go",
    "content": "package net\n\nimport (\n\t\"bytes\"\n\t\"context\"\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/alist-org/alist/v3/pkg/utils\"\n\n\t\"github.com/alist-org/alist/v3/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 * 10\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 == -1 {\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 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 = fmt.Errorf(\"ExceedMaxConcurrency\")\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\td.ctx, d.cancel = context.WithCancelCause(d.ctx)\n\n\tmaxPart := int(d.params.Range.Length / int64(d.cfg.PartSize))\n\tif d.params.Range.Length%int64(d.cfg.PartSize) > 0 {\n\t\tmaxPart++\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 d.cfg.Concurrency == 1 {\n\t\tif d.cfg.ConcurrencyLimit != nil {\n\t\t\tgo func() {\n\t\t\t\t<-d.ctx.Done()\n\t\t\t\td.concurrencyFinish()\n\t\t\t}()\n\t\t}\n\t\tresp, err := d.cfg.HttpClient(d.ctx, d.params)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn resp.Body, nil\n\t}\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\td.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\tbuf.Reset(int(finalSize))\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\tif d.written != d.params.Range.Length {\n\t\tlog.Debugf(\"Downloader interrupt before finish\")\n\t\tif d.getErr() == nil {\n\t\t\td.setErr(fmt.Errorf(\"interrupted\"))\n\t\t}\n\t}\n\td.cancel(d.err)\n\tdefer func() {\n\t\tclose(d.chunkChannel)\n\t\tfor _, buf := range d.bufs {\n\t\t\tbuf.Close()\n\t\t}\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 d.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\td.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\t//defer d.wg.Done()\n\tfor {\n\t\tc, ok := <-d.chunkChannel\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif d.getErr() != nil {\n\t\t\t// Drain the channel if there is an error, to prevent deadlocking\n\t\t\t// of download producer.\n\t\t\tbreak\n\t\t}\n\t\tif err := d.downloadChunk(&c); err != nil {\n\t\t\tif err == errCancelConcurrency {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err == context.Canceled {\n\t\t\t\tif e := context.Cause(d.ctx); e != nil {\n\t\t\t\t\terr = e\n\t\t\t\t}\n\t\t\t}\n\t\t\td.setErr(err)\n\t\t\td.cancel(err)\n\t\t}\n\t}\n\td.concurrencyFinish()\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// 测试：下载时 断开 alist向云盘发起的下载连接\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 = fmt.Errorf(\"cancel concurrency\")\nvar errInfiniteRetry = fmt.Errorf(\"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\tif resp == nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {\n\t\t\treturn 0, err\n\t\t}\n\t\tif ch.id == 0 { //第1个任务 有限的重试，超过重试就会结束请求\n\t\t\tswitch resp.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: fmt.Errorf(\"http request failure,status: %d\", resp.StatusCode)}\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\td.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\n\tres, err := RequestHttp(ctx, \"GET\", header, params.URL)\n\tif err != nil {\n\t\treturn res, err\n\t}\n\treturn res, nil\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\tbuffer *bytes.Buffer\n\tsize   int //expected size\n\tctx    context.Context\n\toff    int\n\trw     sync.Mutex\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\treturn &Buf{\n\t\tctx:    ctx,\n\t\tbuffer: bytes.NewBuffer(make([]byte, 0, maxSize)),\n\t\tsize:   maxSize,\n\t}\n}\nfunc (br *Buf) Reset(size int) {\n\tbr.buffer.Reset()\n\tbr.size = size\n\tbr.off = 0\n}\n\nfunc (br *Buf) Read(p []byte) (n int, err 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.off >= br.size {\n\t\treturn 0, io.EOF\n\t}\n\tbr.rw.Lock()\n\tn, err = br.buffer.Read(p)\n\tbr.rw.Unlock()\n\tif err == nil {\n\t\tbr.off += n\n\t\treturn n, err\n\t}\n\tif err != io.EOF {\n\t\treturn n, err\n\t}\n\tif n != 0 {\n\t\tbr.off += n\n\t\treturn n, nil\n\t}\n\t// n==0, err==io.EOF\n\t// wait for new write for 200ms\n\tselect {\n\tcase <-br.ctx.Done():\n\t\treturn 0, br.ctx.Err()\n\tcase <-time.After(time.Millisecond * 200):\n\t\treturn 0, nil\n\t}\n}\n\nfunc (br *Buf) Write(p []byte) (n int, err error) {\n\tif err := br.ctx.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\tbr.rw.Lock()\n\tdefer br.rw.Unlock()\n\tn, err = br.buffer.Write(p)\n\treturn\n}\n\nfunc (br *Buf) Close() {\n\tbr.buffer = nil\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/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/exp/slices\"\n)\n\nvar buf22MB = make([]byte, 1024*1024*22)\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)/partSize + 1\n\tif int(length)%partSize == 0 {\n\t\tchunkSize--\n\t}\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-3\", \"5-3\", \"8-3\", \"11-1\"}\n\tfor _, rng := range expectRngs {\n\t\tif !slices.Contains(*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, 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\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 := 1, *invocations; e != a {\n\t\tt.Errorf(\"expect %v API calls, got %v\", e, a)\n\t}\n\n\texpectRngs := []string{\"2-10\"}\n\tfor _, rng := range expectRngs {\n\t\tif !slices.Contains(*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 != nil {\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\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/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 = mime.TypeByExtension(filepath.Ext(name))\n\t\tif contentType == \"\" {\n\t\t\t// most modern application can handle the default contentType\n\t\t\tcontentType = \"application/octet-stream\"\n\t\t}\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 := context.WithValue(r.Context(), \"request_header\", r.Header)\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 err == ErrExceedMaxConcurrency {\n\t\t\t\tcode = http.StatusTooManyRequests\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 err == ErrExceedMaxConcurrency {\n\t\t\t\tcode = http.StatusTooManyRequests\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\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 err == ErrExceedMaxConcurrency {\n\t\t\t\tcode = http.StatusTooManyRequests\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 res, fmt.Errorf(\"http request [%s] failure,status: %d response:%s\", URL, res.StatusCode, msg)\n\t}\n\treturn res, nil\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\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}\n}\n"
  },
  {
    "path": "internal/net/util.go",
    "content": "package net\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\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// LimitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.\ntype LimitedReadCloser struct {\n\trc        io.ReadCloser\n\tremaining int\n}\n\nfunc (l *LimitedReadCloser) Read(buf []byte) (int, error) {\n\tif l.remaining <= 0 {\n\t\treturn 0, io.EOF\n\t}\n\n\tif len(buf) > l.remaining {\n\t\tbuf = buf[0:l.remaining]\n\t}\n\n\tn, err := l.rc.Read(buf)\n\tl.remaining -= n\n\n\treturn n, err\n}\n\nfunc (l *LimitedReadCloser) Close() error {\n\treturn l.rc.Close()\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\tvar length_int int\n\tif length > math.MaxInt {\n\t\treturn nil, fmt.Errorf(\"doesnot support length bigger than int32 max \")\n\t}\n\tlength_int = int(length)\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 &LimitedReadCloser{readCloser, length_int}, nil\n}\n"
  },
  {
    "path": "internal/offline_download/115/client.go",
    "content": "package _115\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\n\t\"github.com/alist-org/alist/v3/drivers/115\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/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/all.go",
    "content": "package offline_download\n\nimport (\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/115\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/aria2\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/http\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/pikpak\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/qbit\"\n\t_ \"github.com/alist-org/alist/v3/internal/offline_download/thunder\"\n\t_ \"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/errs\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/pkg/aria2/rpc\"\n\t\"github.com/alist-org/alist/v3/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\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/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\tu := task.Url\n\t// parse url\n\t_u, err := url.Parse(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq, err := http.NewRequestWithContext(task.Ctx(), http.MethodGet, u, nil)\n\tif err != nil {\n\t\treturn err\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\t// If Path is empty, use Hostname; otherwise, filePath euqals TempDir which causes os.Create to fail\n\turlPath := _u.Path\n\tif urlPath == \"\" {\n\t\turlPath = strings.ReplaceAll(_u.Host, \".\", \"_\")\n\t}\n\tfilename := path.Base(urlPath)\n\tif n, err := parseFilenameFromContentDisposition(resp.Header.Get(\"Content-Disposition\")); err == nil {\n\t\tfilename = n\n\t}\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\tfileSize := resp.ContentLength\n\ttask.SetTotalBytes(fileSize)\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\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/drivers/pikpak\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/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/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/drivers/pikpak\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/drivers/thunder\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/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/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/drivers/thunder\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\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/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/alist-org/alist/v3/drivers/115\"\n\t\"github.com/alist-org/alist/v3/drivers/pikpak\"\n\t\"github.com/alist-org/alist/v3/drivers/thunder\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\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)\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 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 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 \"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\t}\n\n\ttaskCreator, _ := ctx.Value(\"user\").(*model.User) // taskCreator is nil when convert failed\n\tt := &DownloadTask{\n\t\tTaskExtension: task.TaskExtension{\n\t\t\tCreator: taskCreator,\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/alist-org/alist/v3/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\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xhofe/tache\"\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.ReinitCtx()\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() == \"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\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 == \"PikPak\" || toolName == \"Thunder\" {\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\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"sort\"\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\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xhofe/tache\"\n\t\"net/http\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\ntype TransferTask struct {\n\ttask.TaskExtension\n\tStatus       string        `json:\"-\"` //don't save status to save space\n\tSrcObjPath   string        `json:\"src_obj_path\"`\n\tDstDirPath   string        `json:\"dst_dir_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\tDeletePolicy DeletePolicy  `json:\"delete_policy\"`\n}\n\nfunc (t *TransferTask) Run() error {\n\tt.ReinitCtx()\n\tt.ClearEndTime()\n\tt.SetStartTime(time.Now())\n\tdefer func() { t.SetEndTime(time.Now()) }()\n\tif t.SrcStorage == nil {\n\t\treturn transferStdPath(t)\n\t} else {\n\t\treturn transferObjPath(t)\n\t}\n}\n\nfunc (t *TransferTask) GetName() string {\n\treturn fmt.Sprintf(\"transfer [%s](%s) to [%s](%s)\", t.SrcStorageMp, t.SrcObjPath, t.DstStorageMp, t.DstDirPath)\n}\n\nfunc (t *TransferTask) GetStatus() string {\n\treturn t.Status\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}\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}\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(\"user\").(*model.User)\n\tfor _, entry := range entries {\n\t\tt := &TransferTask{\n\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\tCreator: taskCreator,\n\t\t\t},\n\t\t\tSrcObjPath:   stdpath.Join(tempDir, entry.Name()),\n\t\t\tDstDirPath:   dstDirActualPath,\n\t\t\tDstStorage:   dstStorage,\n\t\t\tDstStorageMp: dstStorage.GetStorage().MountPath,\n\t\t\tDeletePolicy: deletePolicy,\n\t\t}\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.SrcObjPath)\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.SrcObjPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tsrcRawPath := stdpath.Join(t.SrcObjPath, entry.Name())\n\t\t\tdstObjPath := stdpath.Join(t.DstDirPath, info.Name())\n\t\t\tt := &TransferTask{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t},\n\t\t\t\tSrcObjPath:   srcRawPath,\n\t\t\t\tDstDirPath:   dstObjPath,\n\t\t\t\tDstStorage:   t.DstStorage,\n\t\t\t\tSrcStorageMp: t.SrcStorageMp,\n\t\t\t\tDstStorageMp: t.DstStorageMp,\n\t\t\t\tDeletePolicy: t.DeletePolicy,\n\t\t\t}\n\t\t\tTransferTaskManager.Add(t)\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.SrcObjPath)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to open file %s\", t.SrcObjPath)\n\t}\n\tinfo, err := rc.Stat()\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to get file %s\", t.SrcObjPath)\n\t}\n\tmimetype := utils.GetMimeType(t.SrcObjPath)\n\ts := &stream.FileStream{\n\t\tCtx: nil,\n\t\tObj: &model.Object{\n\t\t\tName:     filepath.Base(t.SrcObjPath),\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(t.Ctx(), t.DstStorage, t.DstDirPath, s, t.SetProgress)\n}\n\nfunc removeStdTemp(t *TransferTask) {\n\tinfo, err := os.Stat(t.SrcObjPath)\n\tif err != nil || info.IsDir() {\n\t\treturn\n\t}\n\tif err := os.Remove(t.SrcObjPath); err != nil {\n\t\tlog.Errorf(\"failed to delete temp file %s, error: %s\", t.SrcObjPath, 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(\"user\").(*model.User) // taskCreator is nil when convert failed\n\tfor _, obj := range objs {\n\t\tt := &TransferTask{\n\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\tCreator: taskCreator,\n\t\t\t},\n\t\t\tSrcObjPath:   stdpath.Join(srcObjActualPath, obj.GetName()),\n\t\t\tDstDirPath:   dstDirActualPath,\n\t\t\tSrcStorage:   srcStorage,\n\t\t\tDstStorage:   dstStorage,\n\t\t\tSrcStorageMp: srcStorage.GetStorage().MountPath,\n\t\t\tDstStorageMp: dstStorage.GetStorage().MountPath,\n\t\t\tDeletePolicy: deletePolicy,\n\t\t}\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.SrcObjPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", t.SrcObjPath)\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.SrcObjPath, model.ListArgs{})\n\t\tif err != nil {\n\t\t\treturn errors.WithMessagef(err, \"failed list src [%s] objs\", t.SrcObjPath)\n\t\t}\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.SrcObjPath, obj.GetName())\n\t\t\tdstObjPath := stdpath.Join(t.DstDirPath, srcObj.GetName())\n\t\t\tTransferTaskManager.Add(&TransferTask{\n\t\t\t\tTaskExtension: task.TaskExtension{\n\t\t\t\t\tCreator: t.Creator,\n\t\t\t\t},\n\t\t\t\tSrcObjPath:   srcObjPath,\n\t\t\t\tDstDirPath:   dstObjPath,\n\t\t\t\tSrcStorage:   t.SrcStorage,\n\t\t\t\tDstStorage:   t.DstStorage,\n\t\t\t\tSrcStorageMp: t.SrcStorageMp,\n\t\t\t\tDstStorageMp: t.DstStorageMp,\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\tsrcFile, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcObjPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get src [%s] file\", t.SrcObjPath)\n\t}\n\tlink, _, err := op.Link(t.Ctx(), t.SrcStorage, t.SrcObjPath, model.LinkArgs{\n\t\tHeader: http.Header{},\n\t})\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] link\", t.SrcObjPath)\n\t}\n\tfs := stream.FileStream{\n\t\tObj: srcFile,\n\t\tCtx: t.Ctx(),\n\t}\n\t// any link provided is seekable\n\tss, err := stream.NewSeekableStream(fs, link)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed get [%s] stream\", t.SrcObjPath)\n\t}\n\tt.SetTotalBytes(srcFile.GetSize())\n\treturn op.Put(t.Ctx(), t.DstStorage, t.DstDirPath, ss, t.SetProgress)\n}\n\nfunc removeObjTemp(t *TransferTask) {\n\tsrcObj, err := op.Get(t.Ctx(), t.SrcStorage, t.SrcObjPath)\n\tif err != nil || srcObj.IsDir() {\n\t\treturn\n\t}\n\tif err := op.Remove(t.Ctx(), t.SrcStorage, t.SrcObjPath); err != nil {\n\t\tlog.Errorf(\"failed to delete temp obj %s, error: %s\", t.SrcObjPath, 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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar archiveMetaCache = cache.NewMemCache(cache.WithShards[*model.ArchiveMetaProvider](64))\nvar archiveMetaG singleflight.Group[*model.ArchiveMetaProvider]\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.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tkey := Key(storage, path)\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\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, cache.WithEx[*model.ArchiveMetaProvider](*m.Expiration))\n\t\t}\n\t\treturn m, nil\n\t}\n\tif storage.Config().OnlyLocal {\n\t\tmeta, err := fn()\n\t\treturn meta, err\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\tbaseName, ext, found := strings.Cut(obj.GetName(), \".\")\n\tif !found {\n\t\tif l.MFile != nil {\n\t\t\t_ = l.MFile.Close()\n\t\t}\n\t\tif l.RangeReadCloser != nil {\n\t\t\t_ = l.RangeReadCloser.Close()\n\t\t}\n\t\treturn nil, nil, nil, errors.Errorf(\"failed get archive tool: the obj does not have an extension.\")\n\t}\n\tpartExt, t, err := tool.GetArchiveTool(\".\" + ext)\n\tif err != nil {\n\t\tvar e error\n\t\tpartExt, t, e = tool.GetArchiveTool(stdpath.Ext(obj.GetName()))\n\t\tif e != nil {\n\t\t\tif l.MFile != nil {\n\t\t\t\t_ = l.MFile.Close()\n\t\t\t}\n\t\t\tif l.RangeReadCloser != nil {\n\t\t\t\t_ = l.RangeReadCloser.Close()\n\t\t\t}\n\t\t\treturn nil, nil, nil, errors.WithMessagef(stderrors.Join(err, e), \"failed get archive tool: %s\", ext)\n\t\t}\n\t}\n\tss, err := stream.NewSeekableStream(stream.FileStream{Ctx: ctx, Obj: obj}, l)\n\tif err != nil {\n\t\tif l.MFile != nil {\n\t\t\t_ = l.MFile.Close()\n\t\t}\n\t\tif l.RangeReadCloser != nil {\n\t\t\t_ = l.RangeReadCloser.Close()\n\t\t}\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} else {\n\t\tindex := partExt.SecondPartIndex\n\t\tdir := stdpath.Dir(path)\n\t\tfor {\n\t\t\tp := stdpath.Join(dir, baseName+fmt.Sprintf(partExt.PartFileFormat, index))\n\t\t\tvar o model.Obj\n\t\t\tl, o, err = Link(ctx, storage, p, args)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tss, err = stream.NewSeekableStream(stream.FileStream{Ctx: ctx, Obj: o}, l)\n\t\t\tif err != nil {\n\t\t\t\tif l.MFile != nil {\n\t\t\t\t\t_ = l.MFile.Close()\n\t\t\t\t}\n\t\t\t\tif l.RangeReadCloser != nil {\n\t\t\t\t\t_ = l.RangeReadCloser.Close()\n\t\t\t\t}\n\t\t\t\tfor _, s := range ret {\n\t\t\t\t\t_ = s.Close()\n\t\t\t\t}\n\t\t\t\treturn nil, nil, nil, errors.WithMessagef(err, \"failed get [%s] stream\", path)\n\t\t\t}\n\t\t\tret = append(ret, ss)\n\t\t\tindex++\n\t\t}\n\t\treturn obj, t, ret, nil\n\t}\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} else if ss[0].Link.MFile == nil {\n\t\t// alias、crypt 驱动\n\t\tarchiveMetaProvider.Expiration = ss[0].Link.Expiration\n\t}\n\treturn obj, archiveMetaProvider, err\n}\n\nvar archiveListCache = cache.NewMemCache(cache.WithShards[[]model.Obj](64))\nvar archiveListG singleflight.Group[[]model.Obj]\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.Errorf(\"storage not init: %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\tobj, files, 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// set path\n\t\tfor _, f := range files {\n\t\t\tif s, ok := f.(model.SetPath); ok && f.GetPath() == \"\" && obj.GetPath() != \"\" {\n\t\t\t\ts.SetPath(stdpath.Join(obj.GetPath(), args.InnerPath, f.GetName()))\n\t\t\t}\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, cache.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, []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, 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\tfiles, err := storageAr.ListArchive(ctx, obj, args.ArchiveInnerArgs)\n\t\tif !errors.Is(err, errs.NotImplement) {\n\t\t\treturn obj, files, 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\tfiles, err := t.List(ss, args.ArchiveInnerArgs)\n\treturn obj, files, err\n}\n\nfunc listArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, []model.Obj, error) {\n\tobj, files, 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, nil, err\n\t\t}\n\t\tfiles, err = getChildrenFromArchiveMeta(meta, args.InnerPath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\tif err == nil && obj == nil {\n\t\tobj, err = GetUnwrap(ctx, storage, path)\n\t}\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn obj, 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.Errorf(\"storage not init: %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 extractLink struct {\n\tLink *model.Link\n\tObj  model.Obj\n}\n\nvar extractCache = cache.NewMemCache(cache.WithShards[*extractLink](16))\nvar extractG singleflight.Group[*extractLink]\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.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tkey := stdpath.Join(Key(storage, path), args.InnerPath)\n\tif link, ok := extractCache.Get(key); ok {\n\t\treturn link.Link, link.Obj, nil\n\t} else if link, ok := extractCache.Get(key + \":\" + args.IP); ok {\n\t\treturn link.Link, link.Obj, nil\n\t}\n\tfn := func() (*extractLink, error) {\n\t\tlink, 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 link.Link.Expiration != nil {\n\t\t\tif link.Link.IPCacheKey {\n\t\t\t\tkey = key + \":\" + args.IP\n\t\t\t}\n\t\t\textractCache.Set(key, link, cache.WithEx[*extractLink](*link.Link.Expiration))\n\t\t}\n\t\treturn link, nil\n\t}\n\tif storage.Config().OnlyLocal {\n\t\tlink, err := fn()\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn link.Link, link.Obj, nil\n\t}\n\tlink, err, _ := extractG.Do(key, fn)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn link.Link, link.Obj, err\n}\n\nfunc driverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*extractLink, 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 &extractLink{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.Errorf(\"storage not init: %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\tswitch s := storage.(type) {\n\tcase driver.ArchiveDecompressResult:\n\t\tvar newObjs []model.Obj\n\t\tnewObjs, err = s.ArchiveDecompress(ctx, srcObj, dstDir, args)\n\t\tif err == nil {\n\t\t\tif newObjs != nil && len(newObjs) > 0 {\n\t\t\t\tfor _, newObj := range newObjs {\n\t\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t\t}\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(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\tClearCache(storage, dstDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn errors.WithStack(err)\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/alist-org/alist/v3/internal/conf\"\n\n\t\"github.com/alist-org/alist/v3/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}\n\tif !config.OnlyProxy && !config.OnlyLocal {\n\t\titems = append(items, []driver.Item{{\n\t\t\tName: \"web_proxy\",\n\t\t\tType: conf.TypeBool,\n\t\t}, {\n\t\t\tName:     \"webdav_policy\",\n\t\t\tType:     conf.TypeSelect,\n\t\t\tOptions:  \"302_redirect,use_proxy_url,native_proxy\",\n\t\t\tDefault:  \"302_redirect\",\n\t\t\tRequired: true,\n\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} else {\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}\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:    \"down_proxy_sign\",\n\t\tType:    conf.TypeBool,\n\t\tDefault: \"true\",\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/alist-org/alist/v3/drivers\"\n\t\"github.com/alist-org/alist/v3/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\"slices\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/generic_sync\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// In order to facilitate adding some other things before and after file op\n\nvar listCache = cache.NewMemCache(cache.WithShards[[]model.Obj](64))\nvar listG singleflight.Group[[]model.Obj]\n\nfunc updateCacheObj(storage driver.Driver, path string, oldObj model.Obj, newObj model.Obj) {\n\tkey := Key(storage, path)\n\tobjs, ok := listCache.Get(key)\n\tif ok {\n\t\tfor i, obj := range objs {\n\t\t\tif obj.GetName() == newObj.GetName() {\n\t\t\t\tobjs = slices.Delete(objs, i, i+1)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor i, obj := range objs {\n\t\t\tif obj.GetName() == oldObj.GetName() {\n\t\t\t\tobjs[i] = newObj\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlistCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))\n\t}\n}\n\nfunc delCacheObj(storage driver.Driver, path string, obj model.Obj) {\n\tkey := Key(storage, path)\n\tobjs, ok := listCache.Get(key)\n\tif ok {\n\t\tfor i, oldObj := range objs {\n\t\t\tif oldObj.GetName() == obj.GetName() {\n\t\t\t\tobjs = append(objs[:i], objs[i+1:]...)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlistCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))\n\t}\n}\n\nvar addSortDebounceMap generic_sync.MapOf[string, func(func())]\n\nfunc addCacheObj(storage driver.Driver, path string, newObj model.Obj) {\n\tkey := Key(storage, path)\n\tobjs, ok := listCache.Get(key)\n\tif ok {\n\t\tfor i, obj := range objs {\n\t\t\tif obj.GetName() == newObj.GetName() {\n\t\t\t\tobjs[i] = newObj\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// Simple separation of files and folders\n\t\tif len(objs) > 0 && objs[len(objs)-1].IsDir() == newObj.IsDir() {\n\t\t\tobjs = append(objs, newObj)\n\t\t} else {\n\t\t\tobjs = append([]model.Obj{newObj}, objs...)\n\t\t}\n\n\t\tif storage.Config().LocalSort {\n\t\t\tdebounce, _ := addSortDebounceMap.LoadOrStore(key, utils.NewDebounce(time.Minute))\n\t\t\tlog.Debug(\"addCacheObj: wait start sort\")\n\t\t\tdebounce(func() {\n\t\t\t\tlog.Debug(\"addCacheObj: start sort\")\n\t\t\t\tmodel.SortFiles(objs, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)\n\t\t\t\taddSortDebounceMap.Delete(key)\n\t\t\t})\n\t\t}\n\n\t\tlistCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))\n\t}\n}\n\nfunc ClearCache(storage driver.Driver, path string) {\n\tobjs, ok := listCache.Get(Key(storage, path))\n\tif ok {\n\t\tfor _, obj := range objs {\n\t\t\tif obj.IsDir() {\n\t\t\t\tClearCache(storage, stdpath.Join(path, obj.GetName()))\n\t\t\t}\n\t\t}\n\t}\n\tlistCache.Del(Key(storage, path))\n}\n\nfunc Key(storage driver.Driver, path string) string {\n\treturn stdpath.Join(storage.GetStorage().MountPath, utils.FixAndCleanPath(path))\n}\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\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn nil, errors.Errorf(\"storage not init: %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 files, ok := listCache.Get(key); ok {\n\t\t\tlog.Debugf(\"use cache when list %s\", path)\n\t\t\treturn files, nil\n\t\t}\n\t}\n\tdir, err := GetUnwrap(ctx, storage, path)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get dir\")\n\t}\n\tlog.Debugf(\"list dir: %+v\", dir)\n\tif !dir.IsDir() {\n\t\treturn nil, errors.WithStack(errs.NotFolder)\n\t}\n\tobjs, err, _ := listG.Do(key, func() ([]model.Obj, error) {\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// set path\n\t\tfor _, f := range files {\n\t\t\tif s, ok := f.(model.SetPath); ok && f.GetPath() == \"\" && dir.GetPath() != \"\" {\n\t\t\t\ts.SetPath(stdpath.Join(dir.GetPath(), f.GetName()))\n\t\t\t}\n\t\t}\n\t\t// warp obj name\n\t\tmodel.WrapObjsName(files)\n\t\t// call hooks\n\t\tgo func(reqPath string, files []model.Obj) {\n\t\t\tHandleObjsUpdateHook(reqPath, files)\n\t\t}(utils.GetFullPath(storage.GetStorage().MountPath, path), files)\n\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 !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\tlistCache.Set(key, files, cache.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\tlistCache.Del(key)\n\t\t\t}\n\t\t}\n\t\treturn files, nil\n\t})\n\treturn objs, err\n}\n\n// Get object from list of files\nfunc Get(ctx context.Context, storage driver.Driver, path string) (model.Obj, error) {\n\tpath = utils.FixAndCleanPath(path)\n\tlog.Debugf(\"op.Get %s\", path)\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 model.WrapObjName(obj), nil\n\t\t}\n\t}\n\n\t// is root folder\n\tif utils.PathEqual(path, \"/\") {\n\t\tvar rootObj model.Obj\n\t\tif getRooter, ok := storage.(driver.GetRooter); ok {\n\t\t\tobj, 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\trootObj = obj\n\t\t} else {\n\t\t\tswitch r := storage.GetAddition().(type) {\n\t\t\tcase driver.IRootId:\n\t\t\t\trootObj = &model.Object{\n\t\t\t\t\tID:       r.GetRootId(),\n\t\t\t\t\tName:     RootName,\n\t\t\t\t\tSize:     0,\n\t\t\t\t\tModified: storage.GetStorage().Modified,\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t}\n\t\t\tcase driver.IRootPath:\n\t\t\t\trootObj = &model.Object{\n\t\t\t\t\tPath:     r.GetRootPath(),\n\t\t\t\t\tName:     RootName,\n\t\t\t\t\tSize:     0,\n\t\t\t\t\tModified: storage.GetStorage().Modified,\n\t\t\t\t\tIsFolder: true,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.Errorf(\"please implement IRootPath or IRootId or GetRooter method\")\n\t\t\t}\n\t\t}\n\t\tif rootObj == nil {\n\t\t\treturn nil, errors.Errorf(\"please implement IRootPath or IRootId or GetRooter method\")\n\t\t}\n\t\treturn &model.ObjWrapName{\n\t\t\tName: RootName,\n\t\t\tObj:  rootObj,\n\t\t}, nil\n\t}\n\n\t// not root folder\n\tdir, name := stdpath.Split(path)\n\tfiles, err := List(ctx, storage, dir, model.ListArgs{})\n\tif err != nil {\n\t\treturn 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 f, 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)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn model.UnwrapObj(obj), err\n}\n\nvar linkCache = cache.NewMemCache(cache.WithShards[*model.Link](16))\nvar linkG singleflight.Group[*model.Link]\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.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tfile, 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 file.IsDir() {\n\t\treturn nil, nil, errors.WithStack(errs.NotFile)\n\t}\n\tkey := Key(storage, path)\n\tif link, ok := linkCache.Get(key); ok {\n\t\treturn link, file, nil\n\t}\n\tfn := func() (*model.Link, error) {\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\tif link.Expiration != nil {\n\t\t\tif link.IPCacheKey {\n\t\t\t\tkey = key + \":\" + args.IP\n\t\t\t}\n\t\t\tlinkCache.Set(key, link, cache.WithEx[*model.Link](*link.Expiration))\n\t\t}\n\t\treturn link, nil\n\t}\n\n\tif storage.Config().OnlyLocal {\n\t\tlink, err := fn()\n\t\treturn link, file, err\n\t}\n\n\tlink, err, _ := linkG.Do(key, fn)\n\treturn link, file, err\n}\n\n// Other api\nfunc Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (interface{}, error) {\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\tif o, ok := storage.(driver.Other); ok {\n\t\treturn o.Other(ctx, model.OtherArgs{\n\t\t\tObj:    obj,\n\t\t\tMethod: args.Method,\n\t\t\tData:   args.Data,\n\t\t})\n\t} else {\n\t\treturn nil, errs.NotImplement\n\t}\n}\n\nvar mkdirG singleflight.Group[interface{}]\n\nfunc MakeDir(ctx context.Context, storage driver.Driver, path string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\tkey := Key(storage, path)\n\t_, err, _ := mkdirG.Do(key, func() (interface{}, error) {\n\t\t// check if dir exists\n\t\tf, err := GetUnwrap(ctx, storage, path)\n\t\tif err != nil {\n\t\t\tif errs.IsObjectNotFound(err) {\n\t\t\t\tparentPath, dirName := stdpath.Split(path)\n\t\t\t\terr = MakeDir(ctx, storage, parentPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.WithMessagef(err, \"failed to make parent dir [%s]\", parentPath)\n\t\t\t\t}\n\t\t\t\tparentDir, err := GetUnwrap(ctx, storage, parentPath)\n\t\t\t\t// this should not happen\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.WithMessagef(err, \"failed to get parent dir [%s]\", parentPath)\n\t\t\t\t}\n\n\t\t\t\tswitch s := storage.(type) {\n\t\t\t\tcase driver.MkdirResult:\n\t\t\t\t\tvar newObj model.Obj\n\t\t\t\t\tnewObj, err = s.MakeDir(ctx, parentDir, dirName)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif newObj != nil {\n\t\t\t\t\t\t\taddCacheObj(storage, parentPath, model.WrapObjName(newObj))\n\t\t\t\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\t\t\t\tClearCache(storage, parentPath)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase driver.Mkdir:\n\t\t\t\t\terr = s.MakeDir(ctx, parentDir, dirName)\n\t\t\t\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\t\t\t\tClearCache(storage, parentPath)\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, errs.NotImplement\n\t\t\t\t}\n\t\t\t\treturn nil, errors.WithStack(err)\n\t\t\t}\n\t\t\treturn nil, errors.WithMessage(err, \"failed to check if dir exists\")\n\t\t}\n\t\t// dir exists\n\t\tif f.IsDir() {\n\t\t\treturn nil, nil\n\t\t}\n\t\t// dir to make is a file\n\t\treturn nil, errors.New(\"file exists\")\n\t})\n\treturn err\n}\n\nfunc Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\tsrcRawObj, err := Get(ctx, storage, srcPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\tsrcObj := model.UnwrapObj(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\tsrcDirPath := stdpath.Dir(srcPath)\n\n\tswitch s := storage.(type) {\n\tcase driver.MoveResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.Move(ctx, srcObj, dstDir)\n\t\tif err == nil {\n\t\t\tdelCacheObj(storage, srcDirPath, srcRawObj)\n\t\t\tif newObj != nil {\n\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.Move:\n\t\terr = s.Move(ctx, srcObj, dstDir)\n\t\tif err == nil {\n\t\t\tdelCacheObj(storage, srcDirPath, srcRawObj)\n\t\t\tif !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc Rename(ctx context.Context, storage driver.Driver, srcPath, dstName string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tsrcPath = utils.FixAndCleanPath(srcPath)\n\tsrcRawObj, err := Get(ctx, storage, srcPath)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed to get src object\")\n\t}\n\tsrcObj := model.UnwrapObj(srcRawObj)\n\tsrcDirPath := stdpath.Dir(srcPath)\n\n\tswitch s := storage.(type) {\n\tcase driver.RenameResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.Rename(ctx, srcObj, dstName)\n\t\tif err == nil {\n\t\t\tif newObj != nil {\n\t\t\t\tupdateCacheObj(storage, srcDirPath, srcRawObj, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, srcDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.Rename:\n\t\terr = s.Rename(ctx, srcObj, dstName)\n\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\tClearCache(storage, srcDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn errors.WithStack(err)\n}\n\n// Copy Just copy file[s] in a storage\nfunc Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %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\tswitch s := storage.(type) {\n\tcase driver.CopyResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.Copy(ctx, srcObj, dstDir)\n\t\tif err == nil {\n\t\t\tif newObj != nil {\n\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.Copy:\n\t\terr = s.Copy(ctx, srcObj, dstDir)\n\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\tClearCache(storage, dstDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\treturn errors.WithStack(err)\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.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tif utils.PathEqual(path, \"/\") {\n\t\treturn errors.New(\"delete root folder is not allowed, please goto the manage page to delete the storage instead\")\n\t}\n\tpath = utils.FixAndCleanPath(path)\n\trawObj, err := Get(ctx, storage, path)\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\tdirPath := stdpath.Dir(path)\n\n\tswitch s := storage.(type) {\n\tcase driver.Remove:\n\t\terr = s.Remove(ctx, model.UnwrapObj(rawObj))\n\t\tif err == nil {\n\t\t\tdelCacheObj(storage, dirPath, rawObj)\n\t\t\t// clear folder cache recursively\n\t\t\tif rawObj.IsDir() {\n\t\t\t\tClearCache(storage, path)\n\t\t\t}\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, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\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\t// UrlTree PUT\n\tif storage.GetStorage().Driver == \"UrlTree\" {\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}}\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() + \".alist_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\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\tswitch s := storage.(type) {\n\tcase driver.PutResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.Put(ctx, parentDir, file, up)\n\t\tif err == nil {\n\t\t\tif newObj != nil {\n\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.Put:\n\t\terr = s.Put(ctx, parentDir, file, up)\n\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\tClearCache(storage, dstDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\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\tif err != nil {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tkey := Key(storage, stdpath.Join(dstDirPath, file.GetName()))\n\t\t\t\tlinkCache.Del(key)\n\t\t\t}\n\t\t}\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\t_, err := GetUnwrap(ctx, storage, stdpath.Join(dstDirPath, dstName))\n\tif err == nil {\n\t\treturn errors.New(\"obj already exists\")\n\t}\n\terr = MakeDir(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to put url\")\n\t}\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to put url\")\n\t}\n\tswitch s := storage.(type) {\n\tcase driver.PutURLResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.PutURL(ctx, dstDir, dstName, url)\n\t\tif err == nil {\n\t\t\tif newObj != nil {\n\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)\n\t\t\t}\n\t\t}\n\tcase driver.PutURL:\n\t\terr = s.PutURL(ctx, dstDir, dstName, url)\n\t\tif err == nil && !utils.IsBool(lazyCache...) {\n\t\t\tClearCache(storage, dstDirPath)\n\t\t}\n\tdefault:\n\t\treturn errs.NotImplement\n\t}\n\tlog.Debugf(\"put url [%s](%s) done\", dstName, url)\n\treturn errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/op/hook.go",
    "content": "package op\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Obj\ntype ObjsUpdateHook = func(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(parent string, objs []model.Obj) {\n\tfor _, hook := range objsUpdateHooks {\n\t\thook(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\tconf.DefaultRole: func(item *model.SettingItem) error {\n\t\tv := strings.TrimSpace(item.Value)\n\t\tif v == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tid, err := strconv.Atoi(v)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\t_, err = GetRole(uint(id))\n\t\treturn err\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/label.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc DeleteLabelById(ctx context.Context, id, userId uint) error {\n\t_, err := db.GetLabelById(id)\n\tif err != nil {\n\t\treturn errors.WithMessage(err, \"failed get label\")\n\t}\n\n\tif db.GetLabelFileBinDingByLabelIdExists(id, userId) {\n\t\treturn errors.New(\"label have binding relationships\")\n\t}\n\n\t// delete the label in the database\n\tif err := db.DeleteLabelById(id); err != nil {\n\t\treturn errors.WithMessage(err, \"failed delete label in database\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/op/label_file_binding.go",
    "content": "package op\n\nimport (\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype CreateLabelFileBinDingReq struct {\n\tId          string    `json:\"id\"`\n\tPath        string    `json:\"path\"`\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\tLabelIds    string    `json:\"label_ids\"`\n\tLabelIDs    []uint64  `json:\"labelIdList\"`\n}\n\ntype ObjLabelResp struct {\n\tId          string        `json:\"id\"`\n\tPath        string        `json:\"path\"`\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\tLabelList   []model.Label `json:\"label_list\"`\n}\n\nfunc GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {\n\tlabelIds, err := db.GetLabelIds(userId, fileName)\n\tif err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed get label_file_binding\")\n\t}\n\tvar labels []model.Label\n\tif len(labelIds) > 0 {\n\t\tif labels, err = db.GetLabelByIds(labelIds); err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed labels in database\")\n\t\t}\n\t}\n\treturn labels, nil\n}\n\nfunc GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {\n\treturn db.GetLabelsByFileNamesPublic(fileNames)\n}\n\nfunc CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {\n\tif err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {\n\t\treturn errors.WithMessage(err, \"failed del label_file_bin_ding in database\")\n\t}\n\n\tids, err := collectLabelIDs(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, id := range ids {\n\t\tif err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed labels in database\")\n\t\t}\n\t}\n\n\tif !db.GetFileByNameExists(req.Name) {\n\t\tobjFile := model.ObjFile{\n\t\t\tId:          req.Id,\n\t\t\tUserId:      userId,\n\t\t\tPath:        req.Path,\n\t\t\tName:        req.Name,\n\t\t\tSize:        req.Size,\n\t\t\tIsDir:       req.IsDir,\n\t\t\tModified:    req.Modified,\n\t\t\tCreated:     req.Created,\n\t\t\tSign:        req.Sign,\n\t\t\tThumb:       req.Thumb,\n\t\t\tType:        req.Type,\n\t\t\tHashInfoStr: req.HashInfoStr,\n\t\t}\n\t\tif err := db.CreateObjFile(objFile); err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed file in database\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {\n\tlabelMap := strings.Split(labelId, \",\")\n\tvar labelIds []uint\n\tvar labelsFile []model.LabelFileBinding\n\tvar labels []model.Label\n\tvar labelsFileMap = make(map[string][]model.Label)\n\tvar labelsMap = make(map[uint]model.Label)\n\tif labelIds, err = StringSliceToUintSlice(labelMap); err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed string to uint err\")\n\t}\n\t//查询标签信息\n\tif labels, err = db.GetLabelByIds(labelIds); err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed labels in database\")\n\t}\n\tfor _, val := range labels {\n\t\tlabelsMap[val.ID] = val\n\t}\n\t//查询标签对应文件名列表\n\tif labelsFile, err = db.GetLabelFileBinDingByLabelId(labelIds, userId); err != nil {\n\t\treturn nil, errors.WithMessage(err, \"failed labels in database\")\n\t}\n\tfor _, value := range labelsFile {\n\t\tvar labelTemp model.Label\n\t\tlabelTemp = labelsMap[value.LabelId]\n\t\tlabelsFileMap[value.FileName] = append(labelsFileMap[value.FileName], labelTemp)\n\t}\n\tfor index, v := range labelsFileMap {\n\t\tobjFile, err := db.GetFileByName(index, userId)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithMessage(err, \"failed GetFileByName in database\")\n\t\t}\n\t\tobjLabel := ObjLabelResp{\n\t\t\tId:          objFile.Id,\n\t\t\tPath:        objFile.Path,\n\t\t\tName:        objFile.Name,\n\t\t\tSize:        objFile.Size,\n\t\t\tIsDir:       objFile.IsDir,\n\t\t\tModified:    objFile.Modified,\n\t\t\tCreated:     objFile.Created,\n\t\t\tSign:        objFile.Sign,\n\t\t\tThumb:       objFile.Thumb,\n\t\t\tType:        objFile.Type,\n\t\t\tHashInfoStr: objFile.HashInfoStr,\n\t\t\tLabelList:   v,\n\t\t}\n\t\tresult = append(result, objLabel)\n\t}\n\treturn result, nil\n}\n\nfunc StringSliceToUintSlice(strSlice []string) ([]uint, error) {\n\tuintSlice := make([]uint, len(strSlice))\n\tfor i, str := range strSlice {\n\t\t// 使用strconv.ParseUint将字符串转换为uint64\n\t\tuint64Value, err := strconv.ParseUint(str, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, err // 如果转换失败，返回错误\n\t\t}\n\t\t// 将uint64值转换为uint（注意：这里可能存在精度损失，如果uint64值超出了uint的范围）\n\t\tuintSlice[i] = uint(uint64Value)\n\t}\n\treturn uintSlice, nil\n}\n\nfunc RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {\n\treturn db.RestoreLabelFileBindings(bindings, keepIDs, override)\n}\n\nfunc collectLabelIDs(req CreateLabelFileBinDingReq) ([]uint64, error) {\n\tif len(req.LabelIDs) > 0 {\n\t\treturn req.LabelIDs, nil\n\t}\n\ts := strings.TrimSpace(req.LabelIds)\n\tif s == \"\" {\n\t\treturn nil, nil\n\t}\n\treplacer := strings.NewReplacer(\"，\", \",\", \"、\", \",\", \"；\", \",\", \";\", \",\")\n\ts = replacer.Replace(s)\n\tparts := strings.Split(s, \",\")\n\tids := make([]uint64, 0, len(parts))\n\tfor _, p := range parts {\n\t\tp = strings.TrimSpace(p)\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tid, err := strconv.ParseUint(p, 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid label ID '%s': %v\", p, err)\n\t\t}\n\t\tids = append(ids, id)\n\t}\n\treturn ids, nil\n}\n"
  },
  {
    "path": "internal/op/meta.go",
    "content": "package op\n\nimport (\n\tstdpath \"path\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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\t\"github.com/alist-org/alist/v3/internal/errs\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/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/role.go",
    "content": "package op\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\nvar roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))\nvar roleG singleflight.Group[*model.Role]\n\nfunc init() {\n\tmodel.FetchRole = GetRole\n}\n\nfunc enforceAdminRoleDefaults(r *model.Role) error {\n\tif r == nil || r.Name != \"admin\" {\n\t\treturn nil\n\t}\n\tif len(r.PermissionScopes) == 1 {\n\t\tscopePath := utils.FixAndCleanPath(r.PermissionScopes[0].Path)\n\t\tif scopePath == \"/\" && r.PermissionScopes[0].Permission == 0xFFFF {\n\t\t\tr.PermissionScopes[0].Path = \"/\"\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tr.PermissionScopes = []model.PermissionEntry{\n\t\t{Path: \"/\", Permission: 0xFFFF},\n\t}\n\treturn db.UpdateRole(r)\n}\n\nfunc GetRole(id uint) (*model.Role, error) {\n\tkey := fmt.Sprint(id)\n\tif r, ok := roleCache.Get(key); ok {\n\t\treturn r, nil\n\t}\n\tr, err, _ := roleG.Do(key, func() (*model.Role, error) {\n\t\t_r, err := db.GetRole(id)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := enforceAdminRoleDefaults(_r); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\troleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))\n\t\troleCache.Set(_r.Name, _r, cache.WithEx[*model.Role](time.Hour))\n\t\treturn _r, nil\n\t})\n\treturn r, err\n}\n\nfunc GetRoleByName(name string) (*model.Role, error) {\n\tif r, ok := roleCache.Get(name); ok {\n\t\treturn r, nil\n\t}\n\tr, err, _ := roleG.Do(name, func() (*model.Role, error) {\n\t\t_r, err := db.GetRoleByName(name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := enforceAdminRoleDefaults(_r); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\troleCache.Set(name, _r, cache.WithEx[*model.Role](time.Hour))\n\t\troleCache.Set(fmt.Sprint(_r.ID), _r, cache.WithEx[*model.Role](time.Hour))\n\t\treturn _r, nil\n\t})\n\treturn r, err\n}\n\nfunc GetDefaultRoleID() int {\n\titem, err := GetSettingItemByKey(conf.DefaultRole)\n\tif err == nil && item != nil && item.Value != \"\" {\n\t\tif id, err := strconv.Atoi(item.Value); err == nil && id != 0 {\n\t\t\treturn id\n\t\t}\n\t\tif r, err := db.GetRoleByName(item.Value); err == nil {\n\t\t\treturn int(r.ID)\n\t\t}\n\t}\n\tvar r model.Role\n\tif err := db.GetDb().Where(\"`default` = ?\", true).First(&r).Error; err == nil {\n\t\treturn int(r.ID)\n\t}\n\treturn int(model.GUEST)\n}\n\nfunc GetRolesByUserID(userID uint) ([]model.Role, error) {\n\tuser, err := GetUserById(userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar roles []model.Role\n\tfor _, roleID := range user.Role {\n\t\tkey := fmt.Sprint(roleID)\n\n\t\tif r, ok := roleCache.Get(key); ok {\n\t\t\troles = append(roles, *r)\n\t\t\tcontinue\n\t\t}\n\n\t\tr, err, _ := roleG.Do(key, func() (*model.Role, error) {\n\t\t\t_r, err := db.GetRole(uint(roleID))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := enforceAdminRoleDefaults(_r); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\troleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))\n\t\t\troleCache.Set(_r.Name, _r, cache.WithEx[*model.Role](time.Hour))\n\t\t\treturn _r, nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\troles = append(roles, *r)\n\t}\n\n\treturn roles, nil\n}\n\nfunc GetRoles(pageIndex, pageSize int) ([]model.Role, int64, error) {\n\treturn db.GetRoles(pageIndex, pageSize)\n}\n\nfunc CreateRole(r *model.Role) error {\n\tfor i := range r.PermissionScopes {\n\t\tr.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)\n\t}\n\troleCache.Del(fmt.Sprint(r.ID))\n\troleCache.Del(r.Name)\n\tif err := db.CreateRole(r); err != nil {\n\t\treturn err\n\t}\n\tif r.Default {\n\t\troleCache.Clear()\n\t\titem, err := GetSettingItemByKey(conf.DefaultRole)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\titem.Value = strconv.Itoa(int(r.ID))\n\t\tif err := SaveSettingItem(item); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc UpdateRole(r *model.Role) error {\n\told, err := db.GetRole(r.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch old.Name {\n\tcase \"admin\":\n\t\treturn errs.ErrChangeDefaultRole\n\tcase \"guest\":\n\t\tr.Name = \"guest\"\n\t}\n\tfor i := range r.PermissionScopes {\n\t\tr.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)\n\t}\n\t//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&\n\t//\told.PermissionScopes[0].Path != r.PermissionScopes[0].Path {\n\t//\n\t//\toldPath := old.PermissionScopes[0].Path\n\t//\tnewPath := r.PermissionScopes[0].Path\n\t//\n\t//\tusers, err := db.GetUsersByRole(int(r.ID))\n\t//\tif err != nil {\n\t//\t\treturn errors.WithMessage(err, \"failed to get users by role\")\n\t//\t}\n\t//\n\t//\tmodifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)\n\t//\tif err != nil {\n\t//\t\treturn errors.WithMessage(err, \"failed to update user base path when role updated\")\n\t//\t}\n\t//\n\t//\tfor _, name := range modifiedUsernames {\n\t//\t\tuserCache.Del(name)\n\t//\t}\n\t//}\n\troleCache.Del(fmt.Sprint(r.ID))\n\troleCache.Del(r.Name)\n\tif err := db.UpdateRole(r); err != nil {\n\t\treturn err\n\t}\n\tif r.Default {\n\t\troleCache.Clear()\n\t\titem, err := GetSettingItemByKey(conf.DefaultRole)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\titem.Value = strconv.Itoa(int(r.ID))\n\t\tif err := SaveSettingItem(item); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc DeleteRole(id uint) error {\n\told, err := db.GetRole(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif old.Name == \"admin\" || old.Name == \"guest\" {\n\t\treturn errs.ErrChangeDefaultRole\n\t}\n\troleCache.Del(fmt.Sprint(id))\n\troleCache.Del(old.Name)\n\treturn db.DeleteRole(id)\n}\n"
  },
  {
    "path": "internal/op/setting.go",
    "content": "package op\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/pkg/errors\"\n)\n\nvar settingCache = cache.NewMemCache(cache.WithShards[*model.SettingItem](4))\nvar settingG singleflight.Group[*model.SettingItem]\nvar settingCacheF = func(item *model.SettingItem) {\n\tsettingCache.Set(item.Key, item, cache.WithEx[*model.SettingItem](time.Hour))\n}\n\nvar settingGroupCache = cache.NewMemCache(cache.WithShards[[]model.SettingItem](4))\nvar settingGroupG singleflight.Group[[]model.SettingItem]\nvar settingGroupCacheF = func(key string, item []model.SettingItem) {\n\tsettingGroupCache.Set(key, item, cache.WithEx[[]model.SettingItem](time.Hour))\n}\n\nvar settingChangingCallbacks = make([]func(), 0)\n\nfunc RegisterSettingChangingCallback(f func()) {\n\tsettingChangingCallbacks = append(settingChangingCallbacks, f)\n}\n\nfunc SettingCacheUpdate() {\n\tsettingCache.Clear()\n\tsettingGroupCache.Clear()\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, ok := settingGroupCache.Get(\"ALL_SETTING_ITEMS\"); ok {\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, ok := settingGroupCache.Get(\"ALL_PUBLIC_SETTING_ITEMS\"); ok {\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, ok := settingCache.Get(key); ok {\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 := strconv.Itoa(group)\n\tif items, ok := settingGroupCache.Get(key); ok {\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\tkey := strings.Join(utils.MustSliceConvert(groups, func(i int) string {\n\t\treturn strconv.Itoa(i)\n\t}), \",\")\n\n\tif items, ok := settingGroupCache.Get(key); ok {\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\tnoHookItems := make([]model.SettingItem, 0)\n\terrs := make([]error, 0)\n\tfor i := range items {\n\t\tif ok, err := HandleSettingItemHook(&items[i]); ok {\n\t\t\tif err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t} else {\n\t\t\t\terr = db.SaveSettingItem(&items[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tnoHookItems = append(noHookItems, items[i])\n\t\t}\n\t}\n\tif len(noHookItems) > 0 {\n\t\terr := db.SaveSettingItems(noHookItems)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tif len(errs) < len(items)-len(noHookItems)+1 {\n\t\tSettingCacheUpdate()\n\t}\n\treturn utils.MergeErrors(errs...)\n}\n\nfunc SaveSettingItem(item *model.SettingItem) (err error) {\n\t// hook\n\tif _, err := HandleSettingItemHook(item); err != nil {\n\t\treturn err\n\t}\n\t// update\n\tif err = db.SaveSettingItem(item); err != nil {\n\t\treturn 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"
  },
  {
    "path": "internal/op/sshkey.go",
    "content": "package op\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"time\"\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\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/generic_sync\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\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\nfunc firstPathSegment(p string) string {\n\tp = utils.FixAndCleanPath(p)\n\tp = strings.TrimPrefix(p, \"/\")\n\tif p == \"\" {\n\t\treturn \"\"\n\t}\n\tif i := strings.Index(p, \"/\"); i >= 0 {\n\t\treturn p[:i]\n\t}\n\treturn p\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\n\t//if storage.MountPath == \"/\" {\n\t//\treturn 0, errors.New(\"Mount path cannot be '/'\")\n\t//}\n\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\tdriverStorage.SetStatus(err.Error())\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 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\t//if storage.MountPath == \"/\" {\n\t//\treturn errors.New(\"Mount path cannot be '/'\")\n\t//}\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\tmodifiedRoleIDs, err := db.UpdateRolePermissionsPathPrefix(oldStorage.MountPath, storage.MountPath)\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed to update role permissions\")\n\t\t}\n\t\tfor _, id := range modifiedRoleIDs {\n\t\t\troleCache.Del(fmt.Sprint(id))\n\t\t}\n\n\t\t//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)\n\t\t//if err != nil {\n\t\t//\treturn errors.WithMessage(err, \"failed to update user base path\")\n\t\t//}\n\t\tfor _, id := range modifiedRoleIDs {\n\t\t\troleCache.Del(fmt.Sprint(id))\n\n\t\t\tusers, err := db.GetUsersByRole(int(id))\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithMessage(err, \"failed to get users by role\")\n\t\t\t}\n\t\t\tfor _, user := range users {\n\t\t\t\tuserCache.Del(user.Username)\n\t\t\t}\n\t\t}\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\tfirstMount := firstPathSegment(storage.MountPath)\n\tif firstMount != \"\" {\n\t\troles, err := db.GetAllRoles()\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed to load roles\")\n\t\t}\n\t\tusers, err := db.GetAllUsers()\n\t\tif err != nil {\n\t\t\treturn errors.WithMessage(err, \"failed to load users\")\n\t\t}\n\t\tvar usedBy []string\n\t\tfor _, r := range roles {\n\t\t\tfor _, entry := range r.PermissionScopes {\n\t\t\t\tif firstPathSegment(entry.Path) == firstMount {\n\t\t\t\t\tusedBy = append(usedBy, \"role:\"+r.Name)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, u := range users {\n\t\t\tif firstPathSegment(u.BasePath) == firstMount {\n\t\t\t\tusedBy = append(usedBy, \"user:\"+u.Username)\n\t\t\t}\n\t\t}\n\t\tif len(usedBy) > 0 {\n\t\t\treturn errors.Errorf(\"storage is used by %s, please cancel usage first\", strings.Join(usedBy, \", \"))\n\t\t}\n\t}\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\treturn 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\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 nil\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\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\tprefix = utils.FixAndCleanPath(prefix)\n\tset := mapset.NewSet[string]()\n\tfor _, v := range storages {\n\t\tmountPath := utils.GetActualMountPath(v.GetStorage().MountPath)\n\t\t// Exclude prefix itself and non prefix\n\t\tif len(prefix) >= len(mountPath) || !utils.IsSubPath(prefix, mountPath) {\n\t\t\tcontinue\n\t\t}\n\t\tname := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], \"/\"), \"/\", 2)[0]\n\t\tif set.Add(name) {\n\t\t\tfiles = append(files, &model.Object{\n\t\t\t\tName:     name,\n\t\t\t\tSize:     0,\n\t\t\t\tModified: v.GetStorage().Modified,\n\t\t\t\tIsFolder: true,\n\t\t\t})\n\t\t}\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"
  },
  {
    "path": "internal/op/storage_test.go",
    "content": "package op_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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()\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\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/singleflight\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\nvar userCache = cache.NewMemCache(cache.WithShards[*model.User](2))\nvar userG singleflight.Group[*model.User]\nvar guestUser *model.User\nvar adminUser *model.User\n\nfunc enforceAdminUserDefaults(u *model.User) error {\n\tif u == nil || !u.IsAdmin() {\n\t\treturn nil\n\t}\n\tchanged := false\n\tif utils.FixAndCleanPath(u.BasePath) != \"/\" {\n\t\tu.BasePath = \"/\"\n\t\tchanged = true\n\t}\n\tif u.Permission != 0xFFFF {\n\t\tu.Permission = 0xFFFF\n\t\tchanged = true\n\t}\n\tif !changed {\n\t\treturn nil\n\t}\n\treturn db.UpdateUser(u)\n}\n\nfunc GetAdmin() (*model.User, error) {\n\tif adminUser == nil {\n\t\trole, err := GetRoleByName(\"admin\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuser, err := db.GetUserByRole(int(role.ID))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := enforceAdminUserDefaults(user); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tadminUser = user\n\t} else if err := enforceAdminUserDefaults(adminUser); err != nil {\n\t\treturn nil, err\n\t}\n\treturn adminUser, nil\n}\n\nfunc GetGuest() (*model.User, error) {\n\tif guestUser == nil {\n\t\trole, err := GetRoleByName(\"guest\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuser, err := db.GetUserByRole(int(role.ID))\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 GetUsersByRole(role int) ([]model.User, error) {\n\treturn db.GetUsersByRole(role)\n}\n\nfunc GetUserByName(username string) (*model.User, error) {\n\tif username == \"\" {\n\t\treturn nil, errs.EmptyUsername\n\t}\n\tif user, ok := userCache.Get(username); ok {\n\t\tif err := enforceAdminUserDefaults(user); err != nil {\n\t\t\treturn nil, err\n\t\t}\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\tif err := enforceAdminUserDefaults(_user); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserCache.Set(username, _user, cache.WithEx[*model.User](time.Hour))\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\n\terr := db.CreateUser(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\troles, err := GetRolesByUserID(u.ID)\n\tif err == nil {\n\t\tfor _, role := range roles {\n\t\t\tif len(role.PermissionScopes) > 0 {\n\t\t\t\tu.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t_ = db.UpdateUser(u)\n\t\tuserCache.Del(u.Username)\n\t}\n\n\treturn nil\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\tuserCache.Del(old.Username)\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\tuserCache.Del(old.Username)\n\tu.BasePath = utils.FixAndCleanPath(u.BasePath)\n\t//if len(u.Role) > 0 {\n\t//\troles, err := GetRolesByUserID(u.ID)\n\t//\tif err == nil {\n\t//\t\tfor _, role := range roles {\n\t//\t\t\tif len(role.PermissionScopes) > 0 {\n\t//\t\t\t\tu.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)\n\t//\t\t\t\tbreak\n\t//\t\t\t}\n\t//\t\t}\n\t//\t}\n\t//}\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\tuserCache.Del(username)\n\treturn nil\n}\n\nfunc CountEnabledAdminsExcluding(userID uint) (int64, error) {\n\tadminRole, err := GetRoleByName(\"admin\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID)\n}\n"
  },
  {
    "path": "internal/search/bleve/init.go",
    "content": "package bleve\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/search/searcher\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/search/searcher\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/mq\"\n\t\"github.com/alist-org/alist/v3/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, \"user\", 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(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\tctx := context.Background()\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\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\tfor i := range objs {\n\t\tif toAdd.Contains(objs[i].GetName()) {\n\t\t\tif !objs[i].IsDir() {\n\t\t\t\tlog.Debugf(\"add index: %s\", path.Join(parent, objs[i].GetName()))\n\t\t\t\terr = Index(ctx, parent, objs[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"update search index error while index new node: %+v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// build index if it's a folder\n\t\t\t\tdir := path.Join(parent, objs[i].GetName())\n\t\t\t\terr = BuildIndex(ctx,\n\t\t\t\t\t[]string{dir},\n\t\t\t\t\tconf.SlicesMap[conf.IgnorePaths],\n\t\t\t\t\tsetting.GetInt(conf.MaxIndexDepth, 20)-strings.Count(dir, \"/\"), false)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"update search index error while build index: %+v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/search/bleve\"\n\t_ \"github.com/alist-org/alist/v3/internal/search/db\"\n\t_ \"github.com/alist-org/alist/v3/internal/search/db_non_full_text\"\n\t_ \"github.com/alist-org/alist/v3/internal/search/meilisearch\"\n)\n"
  },
  {
    "path": "internal/search/meilisearch/init.go",
    "content": "package meilisearch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/search/searcher\"\n\t\"github.com/alist-org/alist/v3/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\tm := Meilisearch{\n\t\t\tClient: meilisearch.NewClient(meilisearch.ClientConfig{\n\t\t\t\tHost:   conf.Conf.Meilisearch.Host,\n\t\t\t\tAPIKey: conf.Conf.Meilisearch.APIKey,\n\t\t\t}),\n\t\t\tIndexUid:             conf.Conf.Meilisearch.IndexPrefix + \"alist\",\n\t\t\tFilterableAttributes: []string{\"parent\", \"is_dir\", \"name\"},\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)\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\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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/search/searcher\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/google/uuid\"\n\t\"github.com/meilisearch/meilisearch-go\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype searchDocument struct {\n\tID string `json:\"id\"`\n\tmodel.SearchNode\n}\n\ntype Meilisearch struct {\n\tClient               *meilisearch.Client\n\tIndexUid             string\n\tFilterableAttributes []string\n\tSearchableAttributes []string\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\tif req.Scope != 0 {\n\t\tmReq.Filter = fmt.Sprintf(\"is_dir = %v\", req.Scope == 1)\n\t}\n\tsearch, err := m.Client.Index(m.IndexUid).Search(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, _ := utils.SliceConvert(nodes, func(src model.SearchNode) (*searchDocument, error) {\n\n\t\treturn &searchDocument{\n\t\t\tID:         uuid.NewString(),\n\t\t\tSearchNode: src,\n\t\t}, nil\n\t})\n\n\t_, err := m.Client.Index(m.IndexUid).AddDocuments(documents)\n\tif err != nil {\n\t\treturn err\n\t}\n\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\terr := m.Client.Index(m.IndexUid).GetDocuments(&meilisearch.DocumentsQuery{\n\t\tFilter: fmt.Sprintf(\"parent = '%s'\", strings.ReplaceAll(parent, \"'\", \"\\\\'\")),\n\t\tLimit:  int64(model.MaxInt),\n\t}, &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 &searchDocument{\n\t\t\tID: src[\"id\"].(string),\n\t\t\tSearchNode: model.SearchNode{\n\t\t\t\tParent: src[\"parent\"].(string),\n\t\t\t\tName:   src[\"name\"].(string),\n\t\t\t\tIsDir:  src[\"is_dir\"].(bool),\n\t\t\t\tSize:   int64(src[\"size\"].(float64)),\n\t\t\t},\n\t\t}, 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}\n\nfunc (m *Meilisearch) getParentsByPrefix(ctx context.Context, parent string) ([]string, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tdefault:\n\t\tparents := []string{parent}\n\t\tget, err := m.getDocumentsByParent(ctx, parent)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, node := range get {\n\t\t\tif node.IsDir {\n\t\t\t\tarr, err := m.getParentsByPrefix(ctx, path.Join(node.Parent, node.Name))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tparents = append(parents, arr...)\n\t\t\t}\n\t\t}\n\t\treturn parents, nil\n\t}\n}\n\nfunc (m *Meilisearch) DelDirChild(ctx context.Context, prefix string) error {\n\tdfs, err := m.getParentsByPrefix(ctx, utils.FixAndCleanPath(prefix))\n\tif err != nil {\n\t\treturn err\n\t}\n\tutils.SliceReplace(dfs, func(src string) string {\n\t\treturn \"'\" + strings.ReplaceAll(src, \"'\", \"\\\\'\") + \"'\"\n\t})\n\ts := fmt.Sprintf(\"parent IN [%s]\", strings.Join(dfs, \",\"))\n\ttask, err := m.Client.Index(m.IndexUid).DeleteDocumentsByFilter(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttaskStatus, err := m.getTaskStatus(ctx, task.TaskUID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif taskStatus != meilisearch.TaskStatusSucceeded {\n\t\treturn fmt.Errorf(\"DelDir failed, task status is %s\", taskStatus)\n\t}\n\treturn nil\n}\n\nfunc (m *Meilisearch) Del(ctx context.Context, prefix string) error {\n\tprefix = utils.FixAndCleanPath(prefix)\n\tdir, name := path.Split(prefix)\n\tget, err := m.getDocumentsByParent(ctx, dir[:len(dir)-1])\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar document *searchDocument\n\tfor _, v := range get {\n\t\tif v.Name == name {\n\t\t\tdocument = v\n\t\t\tbreak\n\t\t}\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\ttask, err := m.Client.Index(m.IndexUid).DeleteDocument(document.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttaskStatus, err := m.getTaskStatus(ctx, task.TaskUID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif taskStatus != meilisearch.TaskStatusSucceeded {\n\t\treturn fmt.Errorf(\"DelDir failed, task status is %s\", taskStatus)\n\t}\n\treturn nil\n}\n\nfunc (m *Meilisearch) Release(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (m *Meilisearch) Clear(ctx context.Context) error {\n\t_, err := m.Client.Index(m.IndexUid).DeleteAllDocuments()\n\treturn err\n}\n\nfunc (m *Meilisearch) getTaskStatus(ctx context.Context, taskUID int64) (meilisearch.TaskStatus, error) {\n\tforTask, err := m.Client.WaitForTask(taskUID, meilisearch.WaitParams{\n\t\tContext:  ctx,\n\t\tInterval: time.Second,\n\t})\n\tif err != nil {\n\t\treturn meilisearch.TaskStatusUnknown, err\n\t}\n\treturn forTask.Status, nil\n}\n"
  },
  {
    "path": "internal/search/search.go",
    "content": "package search\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/drivers/alist_v3\"\n\t\"github.com/alist-org/alist/v3/drivers/base\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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{\"AList V2\", \"AList V3\", \"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 == \"AList V3\" {\n\t\t\t\taddition := storage.GetAddition().(*alist_v3.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{\"AList V2\", \"AList V3\", \"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/session/session.go",
    "content": "package session\n\nimport \"github.com/alist-org/alist/v3/internal/db\"\n\n// MarkInactive marks the session with the given ID as inactive.\nfunc MarkInactive(sessionID string) error {\n\treturn db.MarkInactive(sessionID)\n}\n"
  },
  {
    "path": "internal/setting/setting.go",
    "content": "package setting\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/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"
  },
  {
    "path": "internal/sign/archive.go",
    "content": "package sign\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"golang.org/x/time/rate\"\n\t\"io\"\n\t\"time\"\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 r.Ctx != nil && utils.IsCanceled(r.Ctx) {\n\t\treturn 0, r.Ctx.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\tif r.Ctx == nil {\n\t\t\tr.Ctx = context.Background()\n\t\t}\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 w.Ctx != nil && utils.IsCanceled(w.Ctx) {\n\t\treturn 0, w.Ctx.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\tif w.Ctx == nil {\n\t\t\tw.Ctx = context.Background()\n\t\t}\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 r.Ctx != nil && utils.IsCanceled(r.Ctx) {\n\t\treturn 0, r.Ctx.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\tif r.Ctx == nil {\n\t\t\tr.Ctx = context.Background()\n\t\t}\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 r.Ctx != nil && utils.IsCanceled(r.Ctx) {\n\t\treturn 0, r.Ctx.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\tif r.Ctx == nil {\n\t\t\tr.Ctx = context.Background()\n\t\t}\n\t\terr = r.Limiter.WaitN(r.Ctx, n)\n\t}\n\treturn\n}\n\ntype RateLimitRangeReadCloser struct {\n\tmodel.RangeReadCloserIF\n\tLimiter Limiter\n}\n\nfunc (rrc *RateLimitRangeReadCloser) RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\trc, err := rrc.RangeReadCloserIF.RangeRead(ctx, httpRange)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &RateLimitReader{\n\t\tReader:  rc,\n\t\tLimiter: rrc.Limiter,\n\t\tCtx:     ctx,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/stream/stream.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/sirupsen/logrus\"\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\ttmpFile  *os.File //if present, tmpFile has full content, it will be deleted at last\n\tpeekBuff *bytes.Reader\n}\n\nfunc (f *FileStream) GetSize() int64 {\n\tif f.tmpFile != nil {\n\t\tinfo, err := f.tmpFile.Stat()\n\t\tif err == nil {\n\t\t\treturn info.Size()\n\t\t}\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\tvar err1, err2 error\n\n\terr1 = f.Closers.Close()\n\tif errors.Is(err1, os.ErrClosed) {\n\t\terr1 = nil\n\t}\n\tif f.tmpFile != nil {\n\t\terr2 = os.RemoveAll(f.tmpFile.Name())\n\t\tif err2 != nil {\n\t\t\terr2 = errs.NewErr(err2, \"failed to remove tmpFile [%s]\", f.tmpFile.Name())\n\t\t} else {\n\t\t\tf.tmpFile = nil\n\t\t}\n\t}\n\n\treturn errors.Join(err1, err2)\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// CacheFullInTempFile save all data into tmpFile. Not recommended since it wears disk,\n// and can't start upload until the file is written. It's not thread-safe!\nfunc (f *FileStream) CacheFullInTempFile() (model.File, error) {\n\tif f.tmpFile != nil {\n\t\treturn f.tmpFile, nil\n\t}\n\tif file, ok := f.Reader.(model.File); ok {\n\t\treturn file, nil\n\t}\n\ttmpF, err := utils.CreateTempFile(f.Reader, f.GetSize())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tf.Add(tmpF)\n\tf.tmpFile = tmpF\n\tf.Reader = tmpF\n\treturn tmpF, nil\n}\n\nfunc (f *FileStream) GetFile() model.File {\n\tif f.tmpFile != nil {\n\t\treturn f.tmpFile\n\t}\n\tif file, ok := f.Reader.(model.File); ok {\n\t\treturn file\n\t}\n\treturn nil\n}\n\nconst InMemoryBufMaxSize = 10 // Megabytes\nconst InMemoryBufMaxSizeBytes = InMemoryBufMaxSize * 1024 * 1024\n\n// RangeRead have to cache all data first since only Reader is provided.\n// also support a peeking RangeRead at very start, but won't buffer more than 10MB data in memory\nfunc (f *FileStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {\n\tif httpRange.Length == -1 {\n\t\t// 参考 internal/net/request.go\n\t\thttpRange.Length = f.GetSize() - httpRange.Start\n\t}\n\tsize := httpRange.Start + httpRange.Length\n\tif f.peekBuff != nil && size <= int64(f.peekBuff.Len()) {\n\t\treturn io.NewSectionReader(f.peekBuff, httpRange.Start, httpRange.Length), nil\n\t}\n\tvar cache io.ReaderAt = f.GetFile()\n\tif cache == nil {\n\t\tif size <= InMemoryBufMaxSizeBytes {\n\t\t\tbufSize := min(size, f.GetSize())\n\t\t\t// 使用bytes.Buffer作为io.CopyBuffer的写入对象，CopyBuffer会调用Buffer.ReadFrom\n\t\t\t// 即使被写入的数据量与Buffer.Cap一致，Buffer也会扩大\n\t\t\tbuf := make([]byte, bufSize)\n\t\t\tn, err := io.ReadFull(f.Reader, buf)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif n != int(bufSize) {\n\t\t\t\treturn nil, fmt.Errorf(\"stream RangeRead did not get all data in peek, expect =%d ,actual =%d\", bufSize, n)\n\t\t\t}\n\t\t\tf.peekBuff = bytes.NewReader(buf)\n\t\t\tf.Reader = io.MultiReader(f.peekBuff, f.Reader)\n\t\t\tcache = f.peekBuff\n\t\t} else {\n\t\t\tvar err error\n\t\t\tcache, err = f.CacheFullInTempFile()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\treturn io.NewSectionReader(cache, httpRange.Start, httpRange.Length), nil\n}\n\nvar _ model.FileStreamer = (*SeekableStream)(nil)\nvar _ model.FileStreamer = (*FileStream)(nil)\n\n//var _ seekableStream = (*FileStream)(nil)\n\n// for most internal stream, which is either RangeReadCloser or MFile\n// Any functionality implemented based on SeekableStream should implement a Close method,\n// whose only purpose is to close the SeekableStream object. If such functionality has\n// additional resources that need to be closed, they should be added to the Closer property of\n// the SeekableStream object and be closed together when the SeekableStream object is closed.\ntype SeekableStream struct {\n\tFileStream\n\tLink *model.Link\n\t// should have one of belows to support rangeRead\n\trangeReadCloser model.RangeReadCloserIF\n\tmFile           model.File\n}\n\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\tss := &SeekableStream{FileStream: fs, Link: link}\n\tif ss.Reader != nil {\n\t\tresult, ok := ss.Reader.(model.File)\n\t\tif ok {\n\t\t\tss.mFile = result\n\t\t\tss.Closers.Add(result)\n\t\t\treturn ss, nil\n\t\t}\n\t}\n\tif ss.Link != nil {\n\t\tif ss.Link.MFile != nil {\n\t\t\tmFile := ss.Link.MFile\n\t\t\tif _, ok := mFile.(*os.File); !ok {\n\t\t\t\tmFile = &RateLimitFile{\n\t\t\t\t\tFile:    mFile,\n\t\t\t\t\tLimiter: ServerDownloadLimit,\n\t\t\t\t\tCtx:     fs.Ctx,\n\t\t\t\t}\n\t\t\t}\n\t\t\tss.mFile = mFile\n\t\t\tss.Reader = mFile\n\t\t\tss.Closers.Add(mFile)\n\t\t\treturn ss, nil\n\t\t}\n\t\tif ss.Link.RangeReadCloser != nil {\n\t\t\tss.rangeReadCloser = &RateLimitRangeReadCloser{\n\t\t\t\tRangeReadCloserIF: ss.Link.RangeReadCloser,\n\t\t\t\tLimiter:           ServerDownloadLimit,\n\t\t\t}\n\t\t\tss.Add(ss.rangeReadCloser)\n\t\t\treturn ss, nil\n\t\t}\n\t\tif len(ss.Link.URL) > 0 {\n\t\t\trrc, err := GetRangeReadCloserFromLink(ss.GetSize(), link)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trrc = &RateLimitRangeReadCloser{\n\t\t\t\tRangeReadCloserIF: rrc,\n\t\t\t\tLimiter:           ServerDownloadLimit,\n\t\t\t}\n\t\t\tss.rangeReadCloser = rrc\n\t\t\tss.Add(rrc)\n\t\t\treturn ss, nil\n\t\t}\n\t}\n\tif fs.Reader != nil {\n\t\treturn ss, nil\n\t}\n\treturn nil, fmt.Errorf(\"illegal seekableStream\")\n}\n\n//func (ss *SeekableStream) Peek(length int) {\n//\n//}\n\n// RangeRead is not thread-safe, pls use it in single thread only.\nfunc (ss *SeekableStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {\n\tif httpRange.Length == -1 {\n\t\thttpRange.Length = ss.GetSize() - httpRange.Start\n\t}\n\tif ss.mFile != nil {\n\t\treturn io.NewSectionReader(ss.mFile, httpRange.Start, httpRange.Length), nil\n\t}\n\tif ss.tmpFile != nil {\n\t\treturn io.NewSectionReader(ss.tmpFile, httpRange.Start, httpRange.Length), nil\n\t}\n\tif ss.rangeReadCloser != nil {\n\t\trc, err := ss.rangeReadCloser.RangeRead(ss.Ctx, httpRange)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn rc, nil\n\t}\n\treturn ss.FileStream.RangeRead(httpRange)\n}\n\n//func (f *FileStream) GetReader() io.Reader {\n//\treturn f.Reader\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\t//f.mu.Lock()\n\n\t//f.peekedOnce = true\n\t//defer f.mu.Unlock()\n\tif ss.Reader == nil {\n\t\tif ss.rangeReadCloser == nil {\n\t\t\treturn 0, fmt.Errorf(\"illegal seekableStream\")\n\t\t}\n\t\trc, err := ss.rangeReadCloser.RangeRead(ss.Ctx, http_range.Range{Length: -1})\n\t\tif err != nil {\n\t\t\treturn 0, nil\n\t\t}\n\t\tss.Reader = io.NopCloser(rc)\n\t}\n\treturn ss.Reader.Read(p)\n}\n\nfunc (ss *SeekableStream) CacheFullInTempFile() (model.File, error) {\n\tif ss.tmpFile != nil {\n\t\treturn ss.tmpFile, nil\n\t}\n\tif ss.mFile != nil {\n\t\treturn ss.mFile, nil\n\t}\n\ttmpF, err := utils.CreateTempFile(ss, ss.GetSize())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tss.Add(tmpF)\n\tss.tmpFile = tmpF\n\tss.Reader = tmpF\n\treturn tmpF, nil\n}\n\nfunc (ss *SeekableStream) GetFile() model.File {\n\tif ss.tmpFile != nil {\n\t\treturn ss.tmpFile\n\t}\n\tif ss.mFile != nil {\n\t\treturn ss.mFile\n\t}\n\treturn nil\n}\n\nfunc (f *FileStream) SetTmpFile(r *os.File) {\n\tf.Add(r)\n\tf.tmpFile = r\n\tf.Reader = r\n}\n\ntype ReaderWithSize interface {\n\tio.ReadCloser\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\treturn r.Reader.Close()\n}\n\ntype SStreamReadAtSeeker interface {\n\tmodel.File\n\tGetRawStream() *SeekableStream\n}\n\ntype readerCur struct {\n\treader io.Reader\n\tcur    int64\n}\n\ntype RangeReadReadAtSeeker struct {\n\tss        *SeekableStream\n\tmasterOff int64\n\treaders   []*readerCur\n\theadCache *headCache\n}\n\ntype headCache struct {\n\t*readerCur\n\tbufs [][]byte\n}\n\nfunc (c *headCache) read(p []byte) (n int, err error) {\n\tpL := len(p)\n\tlogrus.Debugf(\"headCache read_%d\", pL)\n\tif c.cur < int64(pL) {\n\t\tbufL := int64(pL) - c.cur\n\t\tbuf := make([]byte, bufL)\n\t\tlr := io.LimitReader(c.reader, bufL)\n\t\toff := 0\n\t\tfor c.cur < int64(pL) {\n\t\t\tn, err = lr.Read(buf[off:])\n\t\t\toff += n\n\t\t\tc.cur += int64(n)\n\t\t\tif err == io.EOF && off == int(bufL) {\n\t\t\t\terr = nil\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tc.bufs = append(c.bufs, buf)\n\t}\n\tn = 0\n\tif c.cur >= int64(pL) {\n\t\tfor i := 0; n < pL; i++ {\n\t\t\tbuf := c.bufs[i]\n\t\t\tr := len(buf)\n\t\t\tif n+r > pL {\n\t\t\t\tr = pL - n\n\t\t\t}\n\t\t\tn += copy(p[n:], buf[:r])\n\t\t}\n\t}\n\treturn\n}\nfunc (r *headCache) Close() error {\n\tfor i := range r.bufs {\n\t\tr.bufs[i] = nil\n\t}\n\tr.bufs = nil\n\treturn nil\n}\n\nfunc (r *RangeReadReadAtSeeker) InitHeadCache() {\n\tif r.ss.Link.MFile == nil && r.masterOff == 0 {\n\t\treader := r.readers[0]\n\t\tr.readers = r.readers[1:]\n\t\tr.headCache = &headCache{readerCur: reader}\n\t\tr.ss.Closers.Add(r.headCache)\n\t}\n}\n\nfunc NewReadAtSeeker(ss *SeekableStream, offset int64, forceRange ...bool) (SStreamReadAtSeeker, error) {\n\tif ss.mFile != nil {\n\t\t_, err := ss.mFile.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &FileReadAtSeeker{ss: ss}, 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\t_, err := r.getReaderAtOffset(offset)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\trc := &readerCur{reader: ss, cur: offset}\n\t\tr.readers = append(r.readers, rc)\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) GetRawStream() *SeekableStream {\n\treturn r.ss\n}\n\nfunc (r *RangeReadReadAtSeeker) getReaderAtOffset(off int64) (*readerCur, error) {\n\tvar rc *readerCur\n\tfor _, reader := range r.readers {\n\t\tif reader.cur == -1 {\n\t\t\tcontinue\n\t\t}\n\t\tif reader.cur == off {\n\t\t\t// logrus.Debugf(\"getReaderAtOffset match_%d\", off)\n\t\t\treturn reader, nil\n\t\t}\n\t\tif reader.cur > 0 && off >= reader.cur && (rc == nil || reader.cur < rc.cur) {\n\t\t\trc = reader\n\t\t}\n\t}\n\tif rc != nil && off-rc.cur <= utils.MB {\n\t\tn, err := utils.CopyWithBufferN(io.Discard, rc.reader, off-rc.cur)\n\t\trc.cur += n\n\t\tif err == io.EOF && rc.cur == off {\n\t\t\terr = nil\n\t\t}\n\t\tif err == nil {\n\t\t\tlogrus.Debugf(\"getReaderAtOffset old_%d\", off)\n\t\t\treturn rc, nil\n\t\t}\n\t\trc.cur = -1\n\t}\n\tlogrus.Debugf(\"getReaderAtOffset new_%d\", off)\n\n\t// Range请求不能超过文件大小，有些云盘处理不了就会返回整个文件\n\treader, err := r.ss.RangeRead(http_range.Range{Start: off, Length: r.ss.GetSize() - off})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trc = &readerCur{reader: reader, cur: off}\n\tr.readers = append(r.readers, rc)\n\treturn rc, nil\n}\n\nfunc (r *RangeReadReadAtSeeker) ReadAt(p []byte, off int64) (int, error) {\n\tif off == 0 && r.headCache != nil {\n\t\treturn r.headCache.read(p)\n\t}\n\trc, err := r.getReaderAtOffset(off)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tn, num := 0, 0\n\tfor num < len(p) {\n\t\tn, err = rc.reader.Read(p[num:])\n\t\trc.cur += int64(n)\n\t\tnum += n\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err == io.EOF {\n\t\t\t// io.EOF是reader读取完了\n\t\t\trc.cur = -1\n\t\t\t// yeka/zip包 没有处理EOF，我们要兼容\n\t\t\t// https://github.com/yeka/zip/blob/03d6312748a9d6e0bc0c9a7275385c09f06d9c14/reader.go#L433\n\t\t\tif num == len(p) {\n\t\t\t\terr = nil\n\t\t\t}\n\t\t}\n\t\tbreak\n\t}\n\treturn num, 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\tif offset == 0 {\n\t\t\treturn r.masterOff, nil\n\t\t}\n\t\toffset += r.masterOff\n\tcase io.SeekEnd:\n\t\toffset += r.ss.GetSize()\n\tdefault:\n\t\treturn 0, errs.NotSupport\n\t}\n\tif offset < 0 {\n\t\treturn r.masterOff, errors.New(\"invalid seek: negative position\")\n\t}\n\tif offset > r.ss.GetSize() {\n\t\treturn r.masterOff, io.EOF\n\t}\n\tr.masterOff = offset\n\treturn offset, nil\n}\n\nfunc (r *RangeReadReadAtSeeker) Read(p []byte) (n int, err error) {\n\tif r.masterOff == 0 && r.headCache != nil {\n\t\treturn r.headCache.read(p)\n\t}\n\trc, err := r.getReaderAtOffset(r.masterOff)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tn, err = rc.reader.Read(p)\n\trc.cur += int64(n)\n\tr.masterOff += int64(n)\n\treturn n, err\n}\n\nfunc (r *RangeReadReadAtSeeker) Close() error {\n\treturn r.ss.Close()\n}\n\ntype FileReadAtSeeker struct {\n\tss *SeekableStream\n}\n\nfunc (f *FileReadAtSeeker) GetRawStream() *SeekableStream {\n\treturn f.ss\n}\n\nfunc (f *FileReadAtSeeker) Read(p []byte) (n int, err error) {\n\treturn f.ss.mFile.Read(p)\n}\n\nfunc (f *FileReadAtSeeker) ReadAt(p []byte, off int64) (n int, err error) {\n\treturn f.ss.mFile.ReadAt(p, off)\n}\n\nfunc (f *FileReadAtSeeker) Seek(offset int64, whence int) (int64, error) {\n\treturn f.ss.mFile.Seek(offset, whence)\n}\n\nfunc (f *FileReadAtSeeker) Close() error {\n\treturn f.ss.Close()\n}\n"
  },
  {
    "path": "internal/stream/util.go",
    "content": "package stream\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/net\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc GetRangeReadCloserFromLink(size int64, link *model.Link) (model.RangeReadCloserIF, error) {\n\tif len(link.URL) == 0 {\n\t\treturn nil, fmt.Errorf(\"can't create RangeReadCloser since URL is empty in link\")\n\t}\n\trangeReaderFunc := func(ctx context.Context, r http_range.Range) (io.ReadCloser, error) {\n\t\tif link.Concurrency != 0 || link.PartSize != 0 {\n\t\t\theader := net.ProcessHeader(nil, link.Header)\n\t\t\tdown := net.NewDownloader(func(d *net.Downloader) {\n\t\t\t\td.Concurrency = link.Concurrency\n\t\t\t\td.PartSize = link.PartSize\n\t\t\t})\n\t\t\treq := &net.HttpRequestParams{\n\t\t\t\tURL:       link.URL,\n\t\t\t\tRange:     r,\n\t\t\t\tSize:      size,\n\t\t\t\tHeaderRef: header,\n\t\t\t}\n\t\t\trc, err := down.Download(ctx, req)\n\t\t\treturn rc, err\n\n\t\t}\n\t\tresponse, err := RequestRangedHttp(ctx, link, r.Start, r.Length)\n\t\tif err != nil {\n\t\t\tif response == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"http request failure, err:%s\", err)\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tif r.Start == 0 && (r.Length == -1 || r.Length == size) || response.StatusCode == http.StatusPartialContent ||\n\t\t\tcheckContentRange(&response.Header, r.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, r.Start, r.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\n\t\treturn response.Body, nil\n\t}\n\tresultRangeReadCloser := model.RangeReadCloser{RangeReader: rangeReaderFunc}\n\treturn &resultRangeReadCloser, nil\n}\n\nfunc RequestRangedHttp(ctx context.Context, link *model.Link, offset, length int64) (*http.Response, error) {\n\theader := net.ProcessHeader(nil, link.Header)\n\theader = http_range.ApplyRangeToHttpHeader(http_range.Range{Start: offset, Length: length}, header)\n\n\treturn net.RequestHttp(ctx, \"GET\", header, link.URL)\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 CacheFullInTempFileAndUpdateProgress(stream model.FileStreamer, up model.UpdateProgress) (model.File, error) {\n\tif cache := stream.GetFile(); cache != nil {\n\t\tup(100)\n\t\treturn cache, nil\n\t}\n\ttmpF, err := utils.CreateTempFile(&ReaderUpdatingProgress{\n\t\tReader:         stream,\n\t\tUpdateProgress: up,\n\t}, stream.GetSize())\n\tif err == nil {\n\t\tstream.SetTmpFile(tmpF)\n\t}\n\treturn tmpF, err\n}\n\nfunc CacheFullInTempFileAndWriter(stream model.FileStreamer, w io.Writer) (model.File, error) {\n\tif cache := stream.GetFile(); cache != nil {\n\t\t_, err := cache.Seek(0, io.SeekStart)\n\t\tif err == nil {\n\t\t\t_, err = utils.CopyWithBuffer(w, cache)\n\t\t\tif err == nil {\n\t\t\t\t_, err = cache.Seek(0, io.SeekStart)\n\t\t\t}\n\t\t}\n\t\treturn cache, err\n\t}\n\ttmpF, err := utils.CreateTempFile(io.TeeReader(stream, w), stream.GetSize())\n\tif err == nil {\n\t\tstream.SetTmpFile(tmpF)\n\t}\n\treturn tmpF, err\n}\n\nfunc CacheFullInTempFileAndHash(stream model.FileStreamer, hashType *utils.HashType, params ...any) (model.File, string, error) {\n\th := hashType.NewFunc(params...)\n\ttmpF, err := CacheFullInTempFileAndWriter(stream, h)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\treturn tmpF, hex.EncodeToString(h.Sum(nil)), err\n}\n"
  },
  {
    "path": "internal/task/base.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/xhofe/tache\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype TaskExtension struct {\n\ttache.Base\n\tctx          context.Context\n\tctxInitMutex sync.Mutex\n\tCreator      *model.User\n\tstartTime    *time.Time\n\tendTime      *time.Time\n\ttotalBytes   int64\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) Ctx() context.Context {\n\tif t.ctx == nil {\n\t\tt.ctxInitMutex.Lock()\n\t\tif t.ctx == nil {\n\t\t\tt.ctx = context.WithValue(t.Base.Ctx(), \"user\", t.Creator)\n\t\t}\n\t\tt.ctxInitMutex.Unlock()\n\t}\n\treturn t.ctx\n}\n\nfunc (t *TaskExtension) ReinitCtx() {\n\tif !conf.Conf.Tasks.AllowRetryCanceled {\n\t\treturn\n\t}\n\tselect {\n\tcase <-t.Base.Ctx().Done():\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tt.SetCtx(ctx)\n\t\tt.SetCancelFunc(cancel)\n\t\tt.ctx = nil\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 \"github.com/xhofe/tache\"\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": "main.go",
    "content": "package main\n\nimport \"github.com/alist-org/alist/v3/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\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tconn.SetWriteDeadline(time.Now().Add(time.Second))\n\t\t\tif err := conn.WriteMessage(websocket.CloseMessage,\n\t\t\t\twebsocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\")); err != nil {\n\t\t\t\tlog.Printf(\"sending websocket close message: %v\", err)\n\t\t\t}\n\t\t\treturn\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\tselect {\n\tcase <-ctx.Done():\n\t\tif err := ctx.Err(); err == context.DeadlineExceeded {\n\t\t\treturn err\n\t\t}\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/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\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\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(f func(ctx context.Context) error) {\n\tif g.sem != nil {\n\t\tg.sem <- token{}\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}\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/alist-org/alist/v3/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/alist-org/alist/v3/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/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\n\t\"github.com/alist-org/alist/v3/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\tvar c = &client{\n\t\turl:    u,\n\t\tclient: http.Client{Jar: jar},\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\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\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(\"POST\", 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\", \"alist-\"+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(\"POST\", 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\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 \\\"alist-\" + 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\", \"alist-\"+id)\n\tresponse, err := c.post(\"/api/v2/torrents/info\", v)\n\tif err != nil {\n\t\treturn TorrentInfo{}, err\n\t}\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\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\tresponse, err := c.post(\"/api/v2/torrents/delete\", v)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif response.StatusCode != 200 {\n\t\treturn errors.New(\"failed to delete qbittorrent task\")\n\t}\n\n\tv = url.Values{}\n\tv.Set(\"tags\", \"alist-\"+id)\n\tresponse, err = c.post(\"/api/v2/torrents/deleteTags\", v)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif response.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\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\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[any]\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 any, 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 any, 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 any, 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 TestPanicDoChan(t *testing.T) {\n\tif runtime.GOOS == \"js\" {\n\t\tt.Skipf(\"js does not support exec\")\n\t}\n\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(os.Args[0], \"-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 runtime.GOOS == \"js\" {\n\t\tt.Skipf(\"js does not support exec\")\n\t}\n\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(os.Args[0], \"-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"
  },
  {
    "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\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 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// forgotten indicates whether Forget was called with this call's key\n\t// while the call was still in flight.\n\tforgotten bool\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\tc.wg.Done()\n\t\tg.mu.Lock()\n\t\tdefer g.mu.Unlock()\n\t\tif !c.forgotten {\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\tif c, ok := g.m[key]; ok {\n\t\tc.forgotten = true\n\t}\n\tdelete(g.m, key)\n\tg.mu.Unlock()\n}\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/alist-org/alist/v3/pkg/generic_sync\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/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/alist-org/alist/v3/internal/errs\"\n\n\t\"github.com/alist-org/alist/v3/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"
  },
  {
    "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/alist-org/alist/v3/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/alist-org/alist/v3/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\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/io.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/exp/constraints\"\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\tAddClosers(closers Closers)\n\tGetClosers() Closers\n}\n\ntype Closers struct {\n\tclosers []io.Closer\n}\n\nfunc (c *Closers) GetClosers() Closers {\n\treturn *c\n}\n\nvar _ ClosersIF = (*Closers)(nil)\n\nfunc (c *Closers) Close() error {\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\treturn errors.Join(errs...)\n}\nfunc (c *Closers) Add(closer io.Closer) {\n\tc.closers = append(c.closers, closer)\n\n}\nfunc (c *Closers) AddClosers(closers Closers) {\n\tc.closers = append(c.closers, closers.closers...)\n}\n\nfunc EmptyClosers() Closers {\n\treturn Closers{[]io.Closer{}}\n}\nfunc NewClosers(c ...io.Closer) Closers {\n\treturn Closers{c}\n}\n\nfunc Min[T constraints.Ordered](a, b T) T {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\nfunc Max[T constraints.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\twritten, err = io.CopyBuffer(dst, src, buff)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn written, nil\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/mask.go",
    "content": "package utils\n\nimport \"strings\"\n\n// MaskIP anonymizes middle segments of an IP address.\nfunc MaskIP(ip string) string {\n\tif ip == \"\" {\n\t\treturn \"\"\n\t}\n\tif strings.Contains(ip, \":\") {\n\t\tparts := strings.Split(ip, \":\")\n\t\tif len(parts) > 2 {\n\t\t\tfor i := 1; i < len(parts)-1; i++ {\n\t\t\t\tif parts[i] != \"\" {\n\t\t\t\t\tparts[i] = \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn strings.Join(parts, \":\")\n\t\t}\n\t\treturn ip\n\t}\n\tparts := strings.Split(ip, \".\")\n\tif len(parts) == 4 {\n\t\tfor i := 1; i < len(parts)-1; i++ {\n\t\t\tparts[i] = \"*\"\n\t\t}\n\t\treturn strings.Join(parts, \".\")\n\t}\n\treturn ip\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/alist-org/alist/v3/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\text := stdpath.Ext(path)\n\tif len(ext) > 0 && ext[0] == '.' {\n\t\text = ext[1:]\n\t}\n\treturn strings.ToLower(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\t/** relative path:\n\t * 1. ..\n\t * 2. ../\n\t * 3. /..\n\t * 4. /../\n\t * 5. /a/b/..\n\t */\n\tif reqPath == \"..\" ||\n\t\tstrings.HasSuffix(reqPath, \"/..\") ||\n\t\tstrings.HasPrefix(reqPath, \"../\") ||\n\t\tstrings.Contains(reqPath, \"/../\") {\n\t\treturn \"\", errs.RelativePath\n\t}\n\n\treqPath = FixAndCleanPath(reqPath)\n\n\tif strings.HasPrefix(reqPath, \"/\") {\n\t\treturn reqPath, nil\n\t}\n\n\treturn stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil\n}\n\nfunc GetFullPath(mountPath, path string) string {\n\treturn stdpath.Join(GetActualMountPath(mountPath), path)\n}\n\n// ValidateNameComponent validates a single path component.\n// It rejects empty names, dot segments, separators, \"..\" sequences, and NUL bytes.\nfunc ValidateNameComponent(name string) error {\n\tif name == \"\" {\n\t\treturn errs.InvalidName\n\t}\n\tif name == \".\" || name == \"..\" {\n\t\treturn errs.InvalidName\n\t}\n\tif strings.Contains(name, \"/\") || strings.Contains(name, \"\\\\\") {\n\t\treturn errs.InvalidName\n\t}\n\tif strings.Contains(name, \"..\") {\n\t\treturn errs.InvalidName\n\t}\n\tif strings.ContainsRune(name, 0) {\n\t\treturn errs.InvalidName\n\t}\n\treturn nil\n}\n\n// JoinUnderBase safely joins baseDir with a single name component and ensures the\n// result stays under baseDir after normalization.\nfunc JoinUnderBase(baseDir, name string) (string, error) {\n\tif err := ValidateNameComponent(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tbase := FixAndCleanPath(baseDir)\n\tjoined := FixAndCleanPath(stdpath.Join(base, name))\n\tif !IsSubPath(base, joined) {\n\t\treturn \"\", errs.InvalidName\n\t}\n\treturn joined, nil\n}\n"
  },
  {
    "path": "pkg/utils/path_test.go",
    "content": "package utils\n\nimport \"testing\"\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 TestValidateNameComponent(t *testing.T) {\n\tvalidNames := []string{\n\t\t\"file.txt\",\n\t\t\"abc\",\n\t\t\"file_name-1\",\n\t}\n\tfor _, name := range validNames {\n\t\tif err := ValidateNameComponent(name); err != nil {\n\t\t\tt.Fatalf(\"expected valid name %q, got error: %v\", name, err)\n\t\t}\n\t}\n\n\tinvalidNames := []string{\n\t\t\"\",\n\t\t\".\",\n\t\t\"..\",\n\t\t\"a/b\",\n\t\t`a\\b`,\n\t\t\"a..b\",\n\t\tstring([]byte{'a', 0, 'b'}),\n\t}\n\tfor _, name := range invalidNames {\n\t\tif err := ValidateNameComponent(name); err == nil {\n\t\t\tt.Fatalf(\"expected invalid name %q to be rejected\", name)\n\t\t}\n\t}\n}\n\nfunc TestJoinUnderBase(t *testing.T) {\n\tbase := \"/lanzou-y/shared/test1\"\n\tout, err := JoinUnderBase(base, \"file.txt\")\n\tif err != nil {\n\t\tt.Fatalf(\"expected join success, got error: %v\", err)\n\t}\n\tif out != \"/lanzou-y/shared/test1/file.txt\" {\n\t\tt.Fatalf(\"unexpected join result: %s\", out)\n\t}\n\n\tif _, err := JoinUnderBase(base, \"../admin/screts.txt\"); err == nil {\n\t\tt.Fatalf(\"expected traversal to be rejected\")\n\t}\n\tif _, err := JoinUnderBase(base, \"sub/child\"); err == nil {\n\t\tt.Fatalf(\"expected nested path to be rejected\")\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 \"alist-\" + 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/alist-org/alist/v3/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/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\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\"fmt\"\n\t\"net/http\"\n\tstdpath \"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n)\n\nfunc GetApiUrl(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"
  },
  {
    "path": "server/common/check.go",
    "content": "package common\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\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// Deprecated: CanAccess is kept for backward compatibility.\n\t// The logic has been moved to CanAccessWithRoles which performs the\n\t// necessary checks based on role permissions. This wrapper ensures\n\t// older calls still work without relying on user permission bits.\n\treturn CanAccessWithRoles(user, meta, reqPath, 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\"net/http\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/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\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 GetHttpReq(ctx context.Context) *http.Request {\n\tif c, ok := ctx.(*gin.Context); ok {\n\t\treturn c.Request\n\t}\n\treturn nil\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/alist-org/alist/v3/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/proxy.go",
    "content": "package common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"maps\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/net\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {\n\tif link.MFile != nil {\n\t\tdefer link.MFile.Close()\n\t\tattachHeader(w, file)\n\t\tcontentType := link.Header.Get(\"Content-Type\")\n\t\tif contentType != \"\" {\n\t\t\tw.Header().Set(\"Content-Type\", contentType)\n\t\t}\n\t\tmFile := link.MFile\n\t\tif _, ok := mFile.(*os.File); !ok {\n\t\t\tmFile = &stream.RateLimitFile{\n\t\t\t\tFile:    mFile,\n\t\t\t\tLimiter: stream.ServerDownloadLimit,\n\t\t\t\tCtx:     r.Context(),\n\t\t\t}\n\t\t}\n\t\thttp.ServeContent(w, r, file.GetName(), file.ModTime(), mFile)\n\t\treturn nil\n\t} else if link.RangeReadCloser != nil {\n\t\tattachHeader(w, file)\n\t\treturn net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{\n\t\t\tRangeReadCloserIF: link.RangeReadCloser,\n\t\t\tLimiter:           stream.ServerDownloadLimit,\n\t\t})\n\t} else if link.Concurrency != 0 || link.PartSize != 0 {\n\t\tattachHeader(w, file)\n\t\tsize := file.GetSize()\n\t\trangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {\n\t\t\trequestHeader := ctx.Value(\"request_header\")\n\t\t\tif requestHeader == nil {\n\t\t\t\trequestHeader = http.Header{}\n\t\t\t}\n\t\t\theader := net.ProcessHeader(requestHeader.(http.Header), link.Header)\n\t\t\tdown := net.NewDownloader(func(d *net.Downloader) {\n\t\t\t\td.Concurrency = link.Concurrency\n\t\t\t\td.PartSize = link.PartSize\n\t\t\t})\n\t\t\treq := &net.HttpRequestParams{\n\t\t\t\tURL:       link.URL,\n\t\t\t\tRange:     httpRange,\n\t\t\t\tSize:      size,\n\t\t\t\tHeaderRef: header,\n\t\t\t}\n\t\t\trc, err := down.Download(ctx, req)\n\t\t\treturn rc, err\n\t\t}\n\t\treturn net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{\n\t\t\tRangeReadCloserIF: &model.RangeReadCloser{RangeReader: rangeReader},\n\t\t\tLimiter:           stream.ServerDownloadLimit,\n\t\t})\n\t} else {\n\t\t//transparent proxy\n\t\theader := net.ProcessHeader(r.Header, link.Header)\n\t\tres, err := net.RequestHttp(r.Context(), r.Method, header, link.URL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer res.Body.Close()\n\n\t\tmaps.Copy(w.Header(), res.Header)\n\t\tw.WriteHeader(res.StatusCode)\n\t\tif r.Method == http.MethodHead {\n\t\t\treturn nil\n\t\t}\n\t\t_, err = utils.CopyWithBuffer(w, &stream.RateLimitReader{\n\t\t\tReader:  res.Body,\n\t\t\tLimiter: stream.ServerDownloadLimit,\n\t\t\tCtx:     r.Context(),\n\t\t})\n\t\treturn err\n\t}\n}\nfunc attachHeader(w http.ResponseWriter, file model.Obj) {\n\tfileName := file.GetName()\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(`attachment; filename=\"%s\"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))\n\tw.Header().Set(\"Content-Type\", utils.GetMimeType(fileName))\n\tw.Header().Set(\"Etag\", GetEtag(file))\n}\nfunc GetEtag(file model.Obj) string {\n\thash := \"\"\n\tfor _, v := range file.GetHash().Export() {\n\t\tif strings.Compare(v, hash) > 0 {\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(), file.GetSize())\n}\n\nvar NoProxyRange = &model.RangeReadCloser{}\n\nfunc ProxyRange(link *model.Link, size int64) {\n\tif link.MFile != nil {\n\t\treturn\n\t}\n\tif link.RangeReadCloser == nil {\n\t\tvar rrc, err = stream.GetRangeReadCloserFromLink(size, link)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"ProxyRange error: %s\", err)\n\t\t\treturn\n\t\t}\n\t\tlink.RangeReadCloser = rrc\n\t} else if link.RangeReadCloser == NoProxyRange {\n\t\tlink.RangeReadCloser = nil\n\t}\n}\n\nfunc BuildDownProxyURL(downProxyURL, path string, useSign bool) string {\n\tbase := strings.Split(downProxyURL, \"\\n\")[0]\n\tif useSign {\n\t\treturn fmt.Sprintf(\"%s%s?sign=%s\", base, utils.EncodePath(path, true), sign.Sign(path))\n\t}\n\treturn fmt.Sprintf(\"%s%s\", base, utils.EncodePath(path, true))\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"
  },
  {
    "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/role_perm.go",
    "content": "package common\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/dlclark/regexp2\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\nconst (\n\tPermSeeHides = iota\n\tPermAccessWithoutPassword\n\tPermAddOfflineDownload\n\tPermWrite\n\tPermRename\n\tPermMove\n\tPermCopy\n\tPermRemove\n\tPermWebdavRead\n\tPermWebdavManage\n\tPermFTPAccess\n\tPermFTPManage\n\tPermReadArchives\n\tPermDecompress\n\tPermPathLimit\n)\n\nfunc HasPermission(perm int32, bit uint) bool {\n\treturn (perm>>bit)&1 == 1\n}\n\nfunc MergeRolePermissions(u *model.User, reqPath string) int32 {\n\tif u == nil {\n\t\treturn 0\n\t}\n\tvar perm int32\n\tfor _, rid := range u.Role {\n\t\trole, err := op.GetRole(uint(rid))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif reqPath == \"/\" || utils.PathEqual(reqPath, u.BasePath) {\n\t\t\tfor _, entry := range role.PermissionScopes {\n\t\t\t\tperm |= entry.Permission\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, entry := range role.PermissionScopes {\n\t\t\t\tif utils.IsSubPath(entry.Path, reqPath) {\n\t\t\t\t\tperm |= entry.Permission\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn perm\n}\n\nfunc CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {\n\tif !CanReadPathByRole(u, reqPath) {\n\t\treturn false\n\t}\n\tperm := MergeRolePermissions(u, reqPath)\n\tif meta != nil && !HasPermission(perm, PermSeeHides) && meta.Hide != \"\" &&\n\t\tIsApply(meta.Path, path.Dir(reqPath), meta.HSub) {\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\tif HasPermission(perm, PermAccessWithoutPassword) {\n\t\treturn true\n\t}\n\tif meta == nil || meta.Password == \"\" {\n\t\treturn true\n\t}\n\tif !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {\n\t\treturn true\n\t}\n\treturn meta.Password == password\n}\n\nfunc CanReadPathByRole(u *model.User, reqPath string) bool {\n\tif u == nil {\n\t\treturn false\n\t}\n\tif reqPath == \"/\" || utils.PathEqual(reqPath, u.BasePath) {\n\t\treturn len(u.Role) > 0\n\t}\n\tfor _, rid := range u.Role {\n\t\trole, err := op.GetRole(uint(rid))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range role.PermissionScopes {\n\t\t\tif utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// HasChildPermission checks whether any child path under reqPath grants the\n// specified permission bit.\nfunc HasChildPermission(u *model.User, reqPath string, bit uint) bool {\n\tif u == nil {\n\t\treturn false\n\t}\n\tfor _, rid := range u.Role {\n\t\trole, err := op.GetRole(uint(rid))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range role.PermissionScopes {\n\t\t\tif utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// CheckPathLimitWithRoles checks whether the path is allowed when the user has\n// the `PermPathLimit` permission for the target path. When the user does not\n// have this permission, the check passes by default.\nfunc CheckPathLimitWithRoles(u *model.User, reqPath string) bool {\n\tperm := MergeRolePermissions(u, reqPath)\n\tif HasPermission(perm, PermPathLimit) {\n\t\treturn CanReadPathByRole(u, reqPath)\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "server/common/sign.go",
    "content": "package common\n\nimport (\n\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/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(ctx *gin.Context) {\n\t\trawPath := ctx.MustGet(\"path\").(string)\n\t\tctx.JSON(200, gin.H{\n\t\t\t\"path\": rawPath,\n\t\t})\n\t})\n\tg.GET(\"/hide_privacy\", func(ctx *gin.Context) {\n\t\tcommon.ErrorStrResp(ctx, \"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\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/spf13/afero\"\n\t\"os\"\n\t\"time\"\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 \"AList 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(\"user\").(*model.User)\n\tpath, err := user.JoinPath(name)\n\tif err != nil {\n\t\treturn nil, err\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, errors.New(\"file already exists\")\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) SetNextFileSize(size int64) {\n\ta.nextFileSize = size\n}\n"
  },
  {
    "path": "server/ftp/fsmanage.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/pkg/errors\"\n\tstdpath \"path\"\n)\n\nfunc Mkdir(ctx context.Context, path string) error {\n\tuser := ctx.Value(\"user\").(*model.User)\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermWrite) || !common.HasPermission(perm, common.PermFTPManage) {\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(\"user\").(*model.User)\n\tperm := common.MergeRolePermissions(user, path)\n\tif !common.HasPermission(perm, common.PermRemove) || !common.HasPermission(perm, common.PermFTPManage) {\n\t\treturn errs.PermissionDenied\n\t}\n\treqPath, err := user.JoinPath(path)\n\tif err != nil {\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(\"user\").(*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\tpermSrc := common.MergeRolePermissions(user, srcPath)\n\tif srcDir == dstDir {\n\t\tif !common.HasPermission(permSrc, common.PermRename) || !common.HasPermission(permSrc, common.PermFTPManage) {\n\t\t\treturn errs.PermissionDenied\n\t\t}\n\t\treturn fs.Rename(ctx, srcPath, dstBase)\n\t} else {\n\t\tif !common.HasPermission(permSrc, common.PermFTPManage) || !common.HasPermission(permSrc, common.PermMove) || (srcBase != dstBase && !common.HasPermission(permSrc, common.PermRename)) {\n\t\t\treturn errs.PermissionDenied\n\t\t}\n\t\tif err = fs.Move(ctx, srcPath, dstDir); err != nil {\n\t\t\tif srcBase != dstBase {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, err1 := fs.Copy(ctx, srcPath, dstDir); err1 != nil {\n\t\t\t\treturn fmt.Errorf(\"failed move for %+v, and failed try copying for %+v\", err, err1)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif srcBase != dstBase {\n\t\t\treturn fs.Rename(ctx, stdpath.Join(dstDir, srcBase), dstBase)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "server/ftp/fsread.go",
    "content": "package ftp\n\nimport (\n\t\"context\"\n\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/pkg/errors\"\n\tfs2 \"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\ntype FileDownloadProxy struct {\n\tftpserver.FileTransfer\n\treader stream.SStreamReadAtSeeker\n}\n\nfunc OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownloadProxy, error) {\n\tuser := ctx.Value(\"user\").(*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, \"meta\", meta)\n\tif !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value(\"meta_pass\").(string)) {\n\t\treturn nil, errs.PermissionDenied\n\t}\n\n\t// directly use proxy\n\theader := *(ctx.Value(\"proxy_header\").(*http.Header))\n\tlink, obj, err := fs.Link(ctx, reqPath, model.LinkArgs{\n\t\tIP:     ctx.Value(\"client_ip\").(string),\n\t\tHeader: header,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileStream := stream.FileStream{\n\t\tObj: obj,\n\t\tCtx: ctx,\n\t}\n\tss, err := stream.NewSeekableStream(fileStream, link)\n\tif err != nil {\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{reader: reader}, nil\n}\n\nfunc (f *FileDownloadProxy) Read(p []byte) (n int, err error) {\n\tn, err = f.reader.Read(p)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = stream.ClientDownloadLimit.WaitN(f.reader.GetRawStream().Ctx, n)\n\treturn\n}\n\nfunc (f *FileDownloadProxy) Write(p []byte) (n int, err error) {\n\treturn 0, errs.NotSupport\n}\n\nfunc (f *FileDownloadProxy) Seek(offset int64, whence int) (int64, error) {\n\treturn f.reader.Seek(offset, whence)\n}\n\nfunc (f *FileDownloadProxy) Close() error {\n\treturn f.reader.Close()\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 = 0755\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(\"user\").(*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, \"meta\", meta)\n\tif !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value(\"meta_pass\").(string)) {\n\t\treturn nil, errs.PermissionDenied\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(\"user\").(*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, \"meta\", meta)\n\tif !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value(\"meta_pass\").(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\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\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/pkg/errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\tstdpath \"path\"\n\t\"time\"\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(\"user\").(*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\tperm := common.MergeRolePermissions(user, path)\n\tif !(common.CanAccessWithRoles(user, meta, path, ctx.Value(\"meta_pass\").(string)) &&\n\t\t((common.HasPermission(perm, common.PermFTPManage) && common.HasPermission(perm, common.PermWrite)) ||\n\t\t\tcommon.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\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\t}\n\terr = stream.ClientUploadLimit.WaitN(f.ctx, n)\n\treturn\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\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}\n\ts.SetTmpFile(f.buffer)\n\t_, err = fs.PutAsTask(f.ctx, dir, s)\n\treturn err\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\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\t}\n\terr = stream.ClientUploadLimit.WaitN(f.ctx, n)\n\treturn\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, true)\n\t}\n}\n"
  },
  {
    "path": "server/ftp/site.go",
    "content": "package ftp\n\nimport (\n\t\"fmt\"\n\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"strconv\"\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.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\tftpserver \"github.com/KirCute/ftpserverlib-pasvportmap\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/server/ftp\"\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\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\theader := &http.Header{}\n\theader.Add(\"User-Agent\", setting.GetStr(conf.FTPProxyUserAgent))\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\tPassiveTransferPortGetter: newPortMapper(setting.GetStr(conf.FTPPasvPortMap)),\n\t\t\tFindPasvPortAttempts:      conf.Conf.FTP.FindPasvPortAttempts,\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\tSiteHandlers: map[string]ftpserver.SiteHandler{\n\t\t\t\t\"SIZE\": ftp.HandleSIZE,\n\t\t\t},\n\t\t},\n\t\tproxyHeader:  header,\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 \"AList 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\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\treturn nil, err\n\t\t}\n\t\tpassHash := model.StaticHash(pass)\n\t\tif err = userObj.ValidatePwdStaticHash(passHash); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tperm := common.MergeRolePermissions(userObj, userObj.BasePath)\n\tif userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {\n\t\treturn nil, errors.New(\"user is not allowed to access via FTP\")\n\t}\n\n\tctx := context.Background()\n\tctx = context.WithValue(ctx, \"user\", userObj)\n\tif user == \"anonymous\" || user == \"guest\" {\n\t\tctx = context.WithValue(ctx, \"meta_pass\", pass)\n\t} else {\n\t\tctx = context.WithValue(ctx, \"meta_pass\", \"\")\n\t}\n\tctx = context.WithValue(ctx, \"client_ip\", cc.RemoteAddr().String())\n\tctx = context.WithValue(ctx, \"proxy_header\", 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.Fatalf(\"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\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\ttype group struct {\n\t\tExposedStart  int\n\t\tListenedStart int\n\t\tLength        int\n\t}\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.Fatalf(\"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 func() (int, int, bool) {\n\t\tidxPort := rand.Intn(totalLength)\n\t\tfor _, g := range groups {\n\t\t\tif idxPort >= g.Length {\n\t\t\t\tidxPort -= g.Length\n\t\t\t} else {\n\t\t\t\treturn g.ExposedStart + idxPort, g.ListenedStart + idxPort, true\n\t\t\t}\n\t\t}\n\t\t// unreachable\n\t\treturn 0, 0, false\n\t}\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\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"net/url\"\n\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/archive/tool\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\tstorageClass, _ := model.GetStorageClass(obj)\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\tStorageClass: storageClass,\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 FsArchiveMeta(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\tuser := c.MustGet(\"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\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermReadArchives) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", 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\tHttpReq: c.Request,\n\t\t},\n\t\tPassword: req.ArchivePass,\n\t}\n\tret, err := fs.ArchiveMeta(c, 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.Request), 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\ntype ArchiveListResp struct {\n\tContent []ObjResp `json:\"content\"`\n\tTotal   int64     `json:\"total\"`\n}\n\nfunc FsArchiveList(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\tuser := c.MustGet(\"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\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermReadArchives) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", 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, 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\tHttpReq: c.Request,\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, ArchiveListResp{\n\t\tContent: ret,\n\t\tTotal:   int64(total),\n\t})\n}\n\ntype StringOrArray []string\n\nfunc (s *StringOrArray) UnmarshalJSON(data []byte) error {\n\tvar value string\n\tif err := json.Unmarshal(data, &value); err == nil {\n\t\t*s = []string{value}\n\t\treturn nil\n\t}\n\tvar sliceValue []string\n\tif err := json.Unmarshal(data, &sliceValue); err != nil {\n\t\treturn err\n\t}\n\t*s = sliceValue\n\treturn nil\n}\n\ntype ArchiveDecompressReq struct {\n\tSrcDir        string        `json:\"src_dir\" form:\"src_dir\"`\n\tDstDir        string        `json:\"dst_dir\" form:\"dst_dir\"`\n\tName          StringOrArray `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}\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.MustGet(\"user\").(*model.User)\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tsrcPaths := make([]string, 0, len(req.Name))\n\tfor _, name := range req.Name {\n\t\tsrcPath, err := utils.JoinUnderBase(srcDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tif !common.CheckPathLimitWithRoles(user, srcPath) {\n\t\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tif !common.CheckPathLimitWithRoles(user, dstDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\ttasks := make([]task.TaskExtensionInfo, 0, len(srcPaths))\n\tfor _, srcPath := range srcPaths {\n\t\tperm := common.MergeRolePermissions(user, srcPath)\n\t\tif !common.HasPermission(perm, common.PermDecompress) {\n\t\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\t\treturn\n\t\t}\n\t\tt, e := fs.ArchiveDecompress(c, 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\tHttpReq: c.Request,\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})\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.MustGet(\"path\").(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.ErrorResp(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, 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\tHttpReq:  c.Request,\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.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tdown(c, link)\n\t}\n}\n\nfunc ArchiveProxy(c *gin.Context) {\n\tarchiveRawPath := c.MustGet(\"path\").(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.ErrorResp(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, 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\tHttpReq: c.Request,\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.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tlocalProxy(c, link, file, storage.GetStorage().ProxyRange)\n\t} else {\n\t\tcommon.ErrorStrResp(c, \"proxy not allowed\", 403)\n\t\treturn\n\t}\n}\n\nfunc ArchiveInternalExtract(c *gin.Context) {\n\tarchiveRawPath := c.MustGet(\"path\").(string)\n\tinnerPath := utils.FixAndCleanPath(c.Query(\"inner\"))\n\tpassword := c.Query(\"pass\")\n\trc, size, err := fs.ArchiveInternalExtract(c, 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\tHttpReq: c.Request,\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.ErrorResp(c, err, 500)\n\t\treturn\n\t}\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\tfilename := stdpath.Base(innerPath)\n\theaders[\"Content-Disposition\"] = fmt.Sprintf(`attachment; filename=\"%s\"; filename*=UTF-8''%s`, filename, url.PathEscape(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 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\"errors\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/device\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/session\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\nvar loginCache = cache.NewMemCache[int]()\nvar (\n\tdefaultDuration            = time.Minute * 5\n\tdefaultTimes               = 5\n\tinvalidLoginCredentialsMsg = \"username or password is incorrect\"\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 := loginCache.Get(ip)\n\tif ok && count >= defaultTimes {\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\tloginCache.Expire(ip, defaultDuration)\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, invalidLoginCredentialsMsg, 400)\n\t\tloginCache.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, invalidLoginCredentialsMsg, 400)\n\t\tloginCache.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\tcommon.ErrorStrResp(c, \"Invalid 2FA code\", 402)\n\t\t\tloginCache.Set(ip, count+1)\n\t\t\treturn\n\t\t}\n\t}\n\n\tclientID := c.GetHeader(\"Client-Id\")\n\tif clientID == \"\" {\n\t\tclientID = c.Query(\"client_id\")\n\t}\n\tkey := utils.GetMD5EncodeStr(fmt.Sprintf(\"%d-%s\",\n\t\tuser.ID, clientID))\n\n\tif err := device.EnsureActiveOnLogin(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {\n\t\tif errors.Is(err, errs.TooManyDevices) {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t} else {\n\t\t\tcommon.ErrorResp(c, err, 400, true)\n\t\t}\n\t\treturn\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, \"device_key\": key})\n\tloginCache.Del(ip)\n}\n\ntype RegisterReq struct {\n\tUsername string `json:\"username\" binding:\"required\"`\n\tPassword string `json:\"password\" binding:\"required\"`\n}\n\n// Register a new user\nfunc Register(c *gin.Context) {\n\tif !setting.GetBool(conf.AllowRegister) {\n\t\tcommon.ErrorStrResp(c, \"registration is disabled\", 403)\n\t\treturn\n\t}\n\tvar req RegisterReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := &model.User{\n\t\tUsername: req.Username,\n\t\tRole:     model.Roles{op.GetDefaultRoleID()},\n\t\tAuthn:    \"[]\",\n\t}\n\tuser.SetPassword(req.Password)\n\tif err := op.CreateUser(user); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\ntype UserResp struct {\n\tmodel.User\n\tOtp         bool                    `json:\"otp\"`\n\tRoleNames   []string                `json:\"role_names\"`\n\tPermissions []model.PermissionEntry `json:\"permissions\"`\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.MustGet(\"user\").(*model.User)\n\n\tuserResp := UserResp{\n\t\tUser: *user,\n\t}\n\tuserResp.Password = \"\"\n\tif userResp.OtpSecret != \"\" {\n\t\tuserResp.Otp = true\n\t}\n\n\tvar roleNames []string\n\tpermMap := map[string]int32{}\n\tpaths := make([]string, 0)\n\n\tfor _, role := range user.RolesDetail {\n\t\troleNames = append(roleNames, role.Name)\n\t\tfor _, entry := range role.PermissionScopes {\n\t\t\tcleanPath := path.Clean(\"/\" + strings.TrimPrefix(entry.Path, \"/\"))\n\t\t\tif _, ok := permMap[cleanPath]; !ok {\n\t\t\t\tpaths = append(paths, cleanPath)\n\t\t\t}\n\t\t\tpermMap[cleanPath] |= entry.Permission\n\t\t}\n\t}\n\tuserResp.RoleNames = roleNames\n\n\tfor _, fullPath := range paths {\n\t\tuserResp.Permissions = append(userResp.Permissions, model.PermissionEntry{\n\t\t\tPath:       fullPath,\n\t\t\tPermission: permMap[fullPath],\n\t\t})\n\t}\n\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.MustGet(\"user\").(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"Guest user can not update profile\", 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.MustGet(\"user\").(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"Guest user can not generate 2FA code\", 403)\n\t\treturn\n\t}\n\tkey, err := totp.Generate(totp.GenerateOpts{\n\t\tIssuer:      \"Alist\",\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.MustGet(\"user\").(*model.User)\n\tif user.IsGuest() {\n\t\tcommon.ErrorStrResp(c, \"Guest user can not generate 2FA code\", 403)\n\t\treturn\n\t}\n\tif !totp.Validate(req.Code, req.Secret) {\n\t\tcommon.ErrorStrResp(c, \"Invalid 2FA code\", 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\tif keyVal, ok := c.Get(\"device_key\"); ok {\n\t\tif err := session.MarkInactive(keyVal.(string)); err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tc.Set(\"session_inactive\", true)\n\t}\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/down.go",
    "content": "package handles\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\tstdpath \"path\"\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/driver\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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.MustGet(\"path\").(string)\n\tfilename := stdpath.Base(rawPath)\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 common.ShouldProxy(storage, filename) {\n\t\tProxy(c)\n\t\treturn\n\t} else {\n\t\tlink, _, err := fs.Link(c, 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\tHttpReq:  c.Request,\n\t\t\tRedirect: true,\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\tdown(c, link)\n\t}\n}\n\nfunc Proxy(c *gin.Context) {\n\trawPath := c.MustGet(\"path\").(string)\n\tfilename := stdpath.Base(rawPath)\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 c.Query(\"type\") == \"preview\" && storage.GetStorage().Driver == \"DoubaoNew\" {\n\t\t// Force proxy for DoubaoNew preview so headers are preserved.\n\t\tlink, file, err := fs.Link(c, rawPath, model.LinkArgs{\n\t\t\tHeader:  c.Request.Header,\n\t\t\tType:    c.Query(\"type\"),\n\t\t\tHttpReq: c.Request,\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\tlocalProxy(c, link, file, storage.GetStorage().ProxyRange)\n\t\treturn\n\t}\n\tif canProxy(storage, filename) {\n\t\tdownProxyUrl := storage.GetStorage().DownProxyUrl\n\t\tif downProxyUrl != \"\" {\n\t\t\t_, ok := c.GetQuery(\"d\")\n\t\t\tif !ok {\n\t\t\t\tURL := common.BuildDownProxyURL(downProxyUrl, rawPath, storage.GetStorage().DownProxySign)\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, rawPath, model.LinkArgs{\n\t\t\tHeader:  c.Request.Header,\n\t\t\tType:    c.Query(\"type\"),\n\t\t\tHttpReq: c.Request,\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\tlocalProxy(c, link, file, storage.GetStorage().ProxyRange)\n\t} else {\n\t\tcommon.ErrorStrResp(c, \"proxy not allowed\", 403)\n\t\treturn\n\t}\n}\n\nfunc down(c *gin.Context, link *model.Link) {\n\tvar err error\n\tif link.MFile != nil {\n\t\tdefer func(ReadSeekCloser io.ReadCloser) {\n\t\t\terr := ReadSeekCloser.Close()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"close data error: %s\", err)\n\t\t\t}\n\t\t}(link.MFile)\n\t}\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.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tc.Redirect(302, link.URL)\n}\n\nfunc localProxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange bool) {\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.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t}\n\tif proxyRange {\n\t\tcommon.ProxyRange(link, file.GetSize())\n\t}\n\tWriter := &common.WrittenResponseWriter{ResponseWriter: c.Writer}\n\n\t//优先处理md文件\n\tif utils.Ext(file.GetName()) == \"md\" && setting.GetBool(conf.FilterReadMeScripts) {\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\tcommon.ErrorResp(c, err, 500, true)\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().WebdavProxy() {\n\t\treturn true\n\t}\n\tif storage.GetStorage().Driver == \"Quark\" && utils.GetFileType(filename) == conf.VIDEO {\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/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/generic\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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.MustGet(\"user\").(*model.User)\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, srcDir) {\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\tif !common.CheckPathLimitWithRoles(user, dstDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, srcDir)\n\tif !common.HasPermission(perm, common.PermMove) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", meta)\n\n\trootFiles, err := fs.List(c, 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, 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, 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\terr := fs.Move(c, 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.MustGet(\"user\").(*model.User)\n\treqPath, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermRename) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", meta)\n\tfor _, renameObject := range req.RenameObjects {\n\t\tif renameObject.SrcName == \"\" || renameObject.NewName == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif err := utils.ValidateNameComponent(renameObject.NewName); err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tfilePath, err := utils.JoinUnderBase(reqPath, renameObject.SrcName)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tif !canRenamePath(c, filePath) {\n\t\t\treturn\n\t\t}\n\t\tif err := fs.Rename(c, 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.MustGet(\"user\").(*model.User)\n\treqPath, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermRename) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", 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, 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\n\t\tif srcRegexp.MatchString(file.GetName()) {\n\t\t\tfilePath, err := utils.JoinUnderBase(reqPath, file.GetName())\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\tif !canRenamePath(c, filePath) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tnewFileName := srcRegexp.ReplaceAllString(file.GetName(), req.NewNameRegex)\n\t\t\tif err := utils.ValidateNameComponent(newFileName); err != nil {\n\t\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := fs.Rename(c, 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\t\"io\"\n\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/generic\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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.MustGet(\"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\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermWrite) {\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, 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}\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.MustGet(\"user\").(*model.User)\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, srcDir) {\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\tif !common.CheckPathLimitWithRoles(user, dstDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tpermMove := common.MergeRolePermissions(user, srcDir)\n\tif !common.HasPermission(permMove, common.PermMove) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tif !req.Overwrite {\n\t\tfor _, name := range req.Names {\n\t\t\tdstPath, err := utils.JoinUnderBase(dstDir, name)\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\tif res, _ := fs.Get(c, dstPath, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", name), 403)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tfor i, name := range req.Names {\n\t\tsrcPath, err := utils.JoinUnderBase(srcDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\t_, err = utils.JoinUnderBase(dstDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\terr = fs.Move(c, srcPath, dstDir, len(req.Names) > i+1)\n\t\tif 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\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.MustGet(\"user\").(*model.User)\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, srcDir) {\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\tif !common.CheckPathLimitWithRoles(user, dstDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, srcDir)\n\tif !common.HasPermission(perm, common.PermCopy) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tif !req.Overwrite {\n\t\tfor _, name := range req.Names {\n\t\t\tdstPath, err := utils.JoinUnderBase(dstDir, name)\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\tif res, _ := fs.Get(c, dstPath, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"file [%s] exists\", name), 403)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tvar addedTasks []task.TaskExtensionInfo\n\tfor i, name := range req.Names {\n\t\tsrcPath, err := utils.JoinUnderBase(srcDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\t_, err = utils.JoinUnderBase(dstDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tt, err := fs.Copy(c, srcPath, dstDir, len(req.Names) > 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\tcommon.SuccessResp(c, gin.H{\n\t\t\"tasks\": getTaskInfos(addedTasks),\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 canRenamePath(c *gin.Context, reqPath string) bool {\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 false\n\t\t}\n\t\treturn true\n\t}\n\tif meta != nil && meta.Password != \"\" && common.IsApply(meta.Path, reqPath, meta.PSub) {\n\t\tcommon.ErrorStrResp(c, \"Path is password-protected and cannot be renamed.\", 403)\n\t\treturn false\n\t}\n\treturn true\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.MustGet(\"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\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tif !canRenamePath(c, reqPath) {\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermRename) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tif err := utils.ValidateNameComponent(req.Name); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif !req.Overwrite {\n\t\tdstPath, err := utils.JoinUnderBase(stdpath.Dir(reqPath), req.Name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\tif dstPath != reqPath {\n\t\t\tif res, _ := fs.Get(c, 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, reqPath, req.Name); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\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.MustGet(\"user\").(*model.User)\n\treqDir, err := user.JoinPath(req.Dir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, reqDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqDir)\n\tif !common.HasPermission(perm, common.PermRemove) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tfor _, name := range req.Names {\n\t\tremovePath, err := utils.JoinUnderBase(reqDir, name)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\treturn\n\t\t}\n\t\terr = fs.Remove(c, removePath)\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.MustGet(\"user\").(*model.User)\n\tsrcDir, err := user.JoinPath(req.SrcDir)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 403)\n\t\treturn\n\t}\n\tif !common.CheckPathLimitWithRoles(user, srcDir) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, srcDir)\n\tif !common.HasPermission(perm, common.PermRemove) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 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\tc.Set(\"meta\", meta)\n\n\trootFiles, err := fs.List(c, 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, 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, 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.MustGet(\"user\").(*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().OnlyLocal {\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.Request),\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, rawPath, model.LinkArgs{IP: c.ClientIP(), Header: c.Request.Header, HttpReq: c.Request})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif link.MFile != nil {\n\t\tdefer func(ReadSeekCloser io.ReadCloser) {\n\t\t\terr := ReadSeekCloser.Close()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"close link data error: %v\", err)\n\t\t\t}\n\t\t}(link.MFile)\n\t}\n\tcommon.SuccessResp(c, link)\n\treturn\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\tId           string                     `json:\"id\"`\n\tPath         string                     `json:\"path\"`\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\tStorageClass string                     `json:\"storage_class,omitempty\"`\n}\n\ntype FsListResp struct {\n\tContent       []ObjLabelResp `json:\"content\"`\n\tTotal         int64          `json:\"total\"`\n\tFilteredTotal int64          `json:\"filtered_total\"`\n\tPage          int            `json:\"page\"`\n\tPerPage       int            `json:\"per_page\"`\n\tHasMore       bool           `json:\"has_more\"`\n\tPagesTotal    int            `json:\"pages_total\"`\n\tReadme        string         `json:\"readme\"`\n\tHeader        string         `json:\"header\"`\n\tWrite         bool           `json:\"write\"`\n\tProvider      string         `json:\"provider\"`\n}\n\ntype ObjLabelResp struct {\n\tId           string                     `json:\"id\"`\n\tPath         string                     `json:\"path\"`\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\tLabelList    []model.Label              `json:\"label_list\"`\n\tStorageClass string                     `json:\"storage_class,omitempty\"`\n}\n\nconst (\n\tDefaultPerPage = 200\n\tMaxPerPage     = 500\n)\n\nfunc FsList(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\teffPage, effPerPage := normalizeListPage(req.Page, req.PerPage)\n\treq.Page = effPage\n\treq.PerPage = effPerPage\n\tuser := c.MustGet(\"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\tc.Set(\"meta\", meta)\n\tif !common.CanAccessWithRoles(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\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermWrite) && !common.CanWrite(meta, reqPath) && req.Refresh {\n\t\tcommon.ErrorStrResp(c, \"Refresh without permission\", 403)\n\t\treturn\n\t}\n\tprovider := \"unknown\"\n\tstorage, storageErr := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})\n\tif storageErr == nil {\n\t\tprovider = storage.GetStorage().Driver\n\t}\n\tobjs, err := fs.List(c, reqPath, &fs.ListArgs{Refresh: req.Refresh})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tfiltered := make([]model.Obj, 0, len(objs))\n\tfor _, obj := range objs {\n\t\tchildPath := stdpath.Join(reqPath, obj.GetName())\n\t\tif common.CanReadPathByRole(user, childPath) {\n\t\t\tfiltered = append(filtered, obj)\n\t\t}\n\t}\n\ttotal, pageObjs := pagination(filtered, &req.PageReq)\n\trespContent := toObjsResp(pageObjs, reqPath, isEncrypt(meta, reqPath))\n\tpagesTotal := calcPagesTotal(total, req.PerPage)\n\thasMore := req.Page*req.PerPage < total\n\n\tcommon.SuccessResp(c, FsListResp{\n\t\tContent:       respContent,\n\t\tTotal:         int64(total),\n\t\tFilteredTotal: int64(total),\n\t\tPage:          req.Page,\n\t\tPerPage:       req.PerPage,\n\t\tHasMore:       hasMore,\n\t\tPagesTotal:    pagesTotal,\n\t\tReadme:        getReadme(meta, reqPath),\n\t\tHeader:        getHeader(meta, reqPath),\n\t\tWrite:         common.HasPermission(perm, common.PermWrite) || common.CanWrite(meta, reqPath),\n\t\tProvider:      provider,\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.MustGet(\"user\").(*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\tc.Set(\"meta\", meta)\n\tif !common.CanAccessWithRoles(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, reqPath, &fs.ListArgs{})\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tvisible := make([]model.Obj, 0, len(objs))\n\tfor _, obj := range objs {\n\t\tchildPath := stdpath.Join(reqPath, obj.GetName())\n\t\tif common.CanReadPathByRole(user, childPath) {\n\t\t\tvisible = append(visible, obj)\n\t\t}\n\t}\n\tdirs := filterDirs(visible)\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 normalizeListPage(page, perPage int) (int, int) {\n\teffPage := page\n\tif effPage <= 0 {\n\t\teffPage = 1\n\t}\n\teffPerPage := perPage\n\tif effPerPage <= 0 {\n\t\teffPerPage = DefaultPerPage\n\t}\n\tif effPerPage > MaxPerPage {\n\t\teffPerPage = MaxPerPage\n\t}\n\treturn effPage, effPerPage\n}\n\nfunc calcPagesTotal(total, perPage int) int {\n\tif total <= 0 || perPage <= 0 {\n\t\treturn 0\n\t}\n\treturn (total + perPage - 1) / perPage\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) []ObjLabelResp {\n\tvar resp []ObjLabelResp\n\n\tnames := make([]string, 0, len(objs))\n\tfor _, obj := range objs {\n\t\tif !obj.IsDir() {\n\t\t\tnames = append(names, obj.GetName())\n\t\t}\n\t}\n\n\tlabelsByName, _ := op.GetLabelsByFileNamesPublic(names)\n\n\tfor _, obj := range objs {\n\t\tvar labels []model.Label\n\t\tif !obj.IsDir() {\n\t\t\tlabels = labelsByName[obj.GetName()]\n\t\t}\n\t\tthumb, _ := model.GetThumb(obj)\n\t\tstorageClass, _ := model.GetStorageClass(obj)\n\t\tresp = append(resp, ObjLabelResp{\n\t\t\tId:           obj.GetID(),\n\t\t\tPath:         obj.GetPath(),\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\tLabelList:    labels,\n\t\t\tStorageClass: storageClass,\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  []ObjLabelResp `json:\"related\"`\n}\n\nfunc FsGet(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\tuser := c.MustGet(\"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\tc.Set(\"meta\", meta)\n\tif !common.CanAccessWithRoles(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, reqPath, &fs.GetArgs{})\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 := \"unknown\"\n\tif 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\tforceProxyRawURL := storage.GetStorage().Driver == \"Quark\" && utils.GetFileType(obj.GetName()) == conf.VIDEO\n\t\tif storage.Config().MustProxy() || storage.GetStorage().WebProxy || forceProxyRawURL {\n\t\t\tquery := \"\"\n\t\t\tif isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {\n\t\t\t\tquery = \"?sign=\" + sign.Sign(reqPath)\n\t\t\t}\n\t\t\tif storage.GetStorage().DownProxyUrl != \"\" {\n\t\t\t\trawURL = common.BuildDownProxyURL(\n\t\t\t\t\tstorage.GetStorage().DownProxyUrl,\n\t\t\t\t\treqPath,\n\t\t\t\t\tstorage.GetStorage().DownProxySign,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\trawURL = fmt.Sprintf(\"%s/p%s%s\",\n\t\t\t\t\tcommon.GetApiUrl(c.Request),\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, 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\tHttpReq:  c.Request,\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\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, 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\tstorageClass, _ := model.GetStorageClass(obj)\n\tcommon.SuccessResp(c, FsGetResp{\n\t\tObjResp: ObjResp{\n\t\t\tId:           obj.GetID(),\n\t\t\tPath:         obj.GetPath(),\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\tStorageClass: storageClass,\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.MustGet(\"user\").(*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\tc.Set(\"meta\", meta)\n\tif !common.CanAccessWithRoles(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, 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/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\nfunc FsStream(c *gin.Context) {\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.MustGet(\"user\").(*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, path, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t_, _ = utils.CopyWithBuffer(io.Discard, c.Request.Body)\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\tsizeStr := c.GetHeader(\"Content-Length\")\n\tsize, err := strconv.ParseInt(sizeStr, 10, 64)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\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 := 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, dir, s)\n\t} else {\n\t\terr = fs.PutDirectly(c, dir, s, true)\n\t}\n\tdefer c.Request.Body.Close()\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif t == nil {\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\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\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.MustGet(\"user\").(*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, path, &fs.GetArgs{NoLog: true}); res != nil {\n\t\t\t_, _ = utils.CopyWithBuffer(io.Discard, c.Request.Body)\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\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, dir, &s)\n\t} else {\n\t\terr = fs.PutDirectly(c, dir, &s, true)\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\"net/url\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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\tfullName := c.Param(\"name\")\n\tUrl := link.String()\n\tUrl = strings.ReplaceAll(Url, \"<\", \"[\")\n\tUrl = strings.ReplaceAll(Url, \">\", \"]\")\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(\"ci.nn.%s\", url.PathEscape(fullName))\n\tsep := \"@\"\n\tif strings.Contains(fullName, sep) {\n\t\tss := strings.Split(fullName, sep)\n\t\tname = strings.Join(ss[:len(ss)-1], sep)\n\t\tidentifier = ss[len(ss)-1]\n\t}\n\n\tname = strings.ReplaceAll(name, \"<\", \"[\")\n\tname = strings.ReplaceAll(name, \">\", \"]\")\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/search\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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/label.go",
    "content": "package handles\n\nimport (\n\t\"errors\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"strconv\"\n)\n\nfunc ListLabel(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\tlabels, total, err := db.GetLabels(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: labels,\n\t\tTotal:   total,\n\t})\n}\n\nfunc GetLabel(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\tlabel, err := db.GetLabelById(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, label)\n}\n\nfunc CreateLabel(c *gin.Context) {\n\tvar req model.Label\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif db.GetLabelByName(req.Name) {\n\t\tcommon.ErrorResp(c, errors.New(\"label name is exists\"), 401)\n\t\treturn\n\t}\n\tif id, err := db.CreateLabel(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 UpdateLabel(c *gin.Context) {\n\tvar req model.Label\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif label, err := db.UpdateLabel(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c, label)\n\t}\n}\n\nfunc DeleteLabel(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\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tif err = op.DeleteLabelById(c, uint(id), userObj.ID); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/label_file_binding.go",
    "content": "package handles\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype DelLabelFileBinDingReq struct {\n\tFileName string `json:\"file_name\"`\n\tLabelId  string `json:\"label_id\"`\n}\n\ntype pageResp[T any] struct {\n\tContent []T   `json:\"content\"`\n\tTotal   int64 `json:\"total\"`\n}\n\ntype restoreLabelBindingsReq struct {\n\tKeepIDs  bool                     `json:\"keep_ids\"`\n\tOverride bool                     `json:\"override\"`\n\tBindings []model.LabelFileBinding `json:\"bindings\"`\n}\n\nfunc GetLabelByFileName(c *gin.Context) {\n\tfileName := c.Query(\"file_name\")\n\tif fileName == \"\" {\n\t\tcommon.ErrorResp(c, errors.New(\"file_name must not empty\"), 400)\n\t\treturn\n\t}\n\tdecodedFileName, err := url.QueryUnescape(fileName)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, errors.New(\"invalid file_name\"), 400)\n\t\treturn\n\t}\n\tfmt.Println(\">>> 原始 fileName:\", fileName)\n\tfmt.Println(\">>> 解码后 fileName:\", decodedFileName)\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tlabels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, labels)\n}\n\nfunc CreateLabelFileBinDing(c *gin.Context) {\n\tvar req op.CreateLabelFileBinDingReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif req.IsDir == true {\n\t\tcommon.ErrorStrResp(c, \"Unable to bind folder\", 400)\n\t\treturn\n\t}\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tif err := op.CreateLabelFileBinDing(req, userObj.ID); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t} else {\n\t\tcommon.SuccessResp(c, gin.H{\n\t\t\t\"msg\": \"添加成功！\",\n\t\t})\n\t}\n}\n\nfunc DelLabelByFileName(c *gin.Context) {\n\tvar req DelLabelFileBinDingReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tlabelId, err := strconv.ParseUint(req.LabelId, 10, 64)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, fmt.Errorf(\"invalid label ID '%s': %v\", req.LabelId, err), 500, true)\n\t\treturn\n\t}\n\tif err = db.DelLabelFileBinDingById(uint(labelId), userObj.ID, req.FileName); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc GetFileByLabel(c *gin.Context) {\n\tlabelId := c.Query(\"label_id\")\n\tif labelId == \"\" {\n\t\tcommon.ErrorResp(c, errors.New(\"file_name must not empty\"), 400)\n\t\treturn\n\t}\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\tfileList, err := op.GetFileByLabel(userObj.ID, labelId)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, fileList)\n}\n\nfunc ListLabelFileBinding(c *gin.Context) {\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\n\tpageStr := c.DefaultQuery(\"page\", \"1\")\n\tsizeStr := c.DefaultQuery(\"page_size\", \"50\")\n\tpage, err := strconv.Atoi(pageStr)\n\tif err != nil || page <= 0 {\n\t\tpage = 1\n\t}\n\tpageSize, err := strconv.Atoi(sizeStr)\n\tif err != nil || pageSize <= 0 || pageSize > 200 {\n\t\tpageSize = 50\n\t}\n\n\tfileName := c.Query(\"file_name\")\n\tlabelIDStr := c.Query(\"label_id\")\n\tvar labelIDs []uint\n\tif labelIDStr != \"\" {\n\t\tparts := strings.Split(labelIDStr, \",\")\n\t\tfor _, p := range parts {\n\t\t\tif p == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tid64, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorResp(c, fmt.Errorf(\"invalid label_id '%s': %v\", p, err), 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlabelIDs = append(labelIDs, uint(id64))\n\t\t}\n\t}\n\n\tlist, total, err := db.ListLabelFileBinDing(userObj.ID, labelIDs, fileName, page, pageSize)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, pageResp[model.LabelFileBinding]{\n\t\tContent: list,\n\t\tTotal:   total,\n\t})\n}\n\nfunc RestoreLabelFileBinding(c *gin.Context) {\n\tvar req restoreLabelBindingsReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif len(req.Bindings) == 0 {\n\t\tcommon.ErrorStrResp(c, \"empty bindings\", 400)\n\t\treturn\n\t}\n\n\tif u, ok := c.Value(\"user\").(*model.User); ok {\n\t\tfor i := range req.Bindings {\n\t\t\tif req.Bindings[i].UserId == 0 {\n\t\t\t\treq.Bindings[i].UserId = u.ID\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := range req.Bindings {\n\t\tb := req.Bindings[i]\n\t\tif b.UserId == 0 || b.LabelId == 0 || strings.TrimSpace(b.FileName) == \"\" {\n\t\t\tcommon.ErrorStrResp(c, \"invalid binding: user_id/label_id/file_name required\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := op.RestoreLabelFileBindings(req.Bindings, req.KeepIDs, req.Override); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"msg\": fmt.Sprintf(\"restored %d rows\", len(req.Bindings)),\n\t})\n}\n\nfunc CreateLabelFileBinDingBatch(c *gin.Context) {\n\tvar req struct {\n\t\tItems []op.CreateLabelFileBinDingReq `json:\"items\" binding:\"required\"`\n\t}\n\tif err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\n\tuserObj, ok := c.Value(\"user\").(*model.User)\n\tif !ok {\n\t\tcommon.ErrorStrResp(c, \"user invalid\", 401)\n\t\treturn\n\t}\n\n\ttype perResult struct {\n\t\tName   string `json:\"name\"`\n\t\tOk     bool   `json:\"ok\"`\n\t\tErrMsg string `json:\"errMsg,omitempty\"`\n\t}\n\tresults := make([]perResult, 0, len(req.Items))\n\tsucceed := 0\n\n\tfor _, item := range req.Items {\n\t\tif item.IsDir {\n\t\t\tresults = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: \"Unable to bind folder\"})\n\t\t\tcontinue\n\t\t}\n\t\tif err := op.CreateLabelFileBinDing(item, userObj.ID); err != nil {\n\t\t\tresults = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: err.Error()})\n\t\t\tcontinue\n\t\t}\n\t\tsucceed++\n\t\tresults = append(results, perResult{Name: item.Name, Ok: true})\n\t}\n\n\tcommon.SuccessResp(c, gin.H{\n\t\t\"total\":   len(req.Items),\n\t\t\"succeed\": succeed,\n\t\t\"failed\":  len(req.Items) - succeed,\n\t\t\"results\": results,\n\t})\n}\n"
  },
  {
    "path": "server/handles/ldap_login.go",
    "content": "package handles\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"gopkg.in/ldap.v3\"\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\tloginLdap(c, &req)\n}\n\nfunc loginLdap(c *gin.Context, req *LoginReq) {\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\n\t// check count of login\n\tip := c.ClientIP()\n\tcount, ok := loginCache.Get(ip)\n\tif ok && count >= defaultTimes {\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\tloginCache.Expire(ip, defaultDuration)\n\t\treturn\n\t}\n\n\t// Auth start\n\tldapServer := setting.GetStr(conf.LdapServer)\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)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to connect to LDAP: %v\", err)\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\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\tutils.Log.Errorf(\"Failed to bind to LDAP: %v\", err)\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\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, req.Username),\n\t\t[]string{\"dn\"},\n\t\tnil,\n\t)\n\tsr, err := l.Search(searchRequest)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"LDAP search failed: %v\", err)\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tif len(sr.Entries) != 1 {\n\t\tutils.Log.Errorf(\"User does not exist or too many entries returned\")\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tuserDN := sr.Entries[0].DN\n\n\t// Bind as the user to verify their password\n\terr = l.Bind(userDN, req.Password)\n\tif err != nil {\n\t\tutils.Log.Errorf(\"Failed to auth. %v\", err)\n\t\tcommon.ErrorResp(c, err, 400)\n\t\tloginCache.Set(ip, count+1)\n\t\treturn\n\t} else {\n\t\tutils.Log.Infof(\"Auth successful username:%s\", req.Username)\n\t}\n\t// Auth finished\n\n\tuser, err := op.GetUserByName(req.Username)\n\tif err != nil {\n\t\tuser, err = ladpRegister(req.Username)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 400)\n\t\t\tloginCache.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\tloginCache.Del(ip)\n}\n\nfunc ladpRegister(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\tID:         0,\n\t\tUsername:   username,\n\t\tPassword:   random.String(16),\n\t\tPermission: int32(setting.GetInt(conf.LdapDefaultPermission, 0)),\n\t\tBasePath:   setting.GetStr(conf.LdapDefaultDir),\n\t\tRole:       nil,\n\t\tDisabled:   false,\n\t}\n\tif err := db.CreateUser(user); err != nil {\n\t\treturn nil, err\n\t}\n\treturn user, nil\n}\n\nfunc dial(ldapServer string) (*ldap.Conn, error) {\n\tvar tlsEnabled bool = 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: conf.Conf.TlsInsecureSkipVerify})\n\t} else {\n\t\treturn ldap.Dial(\"tcp\", ldapServer)\n\t}\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/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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_115 \"github.com/alist-org/alist/v3/drivers/115\"\n\t\"github.com/alist-org/alist/v3/drivers/pikpak\"\n\t\"github.com/alist-org/alist/v3/drivers/thunder\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"github.com/alist-org/alist/v3/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 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\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.MustGet(\"user\").(*model.User)\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\tif !common.CheckPathLimitWithRoles(user, reqPath) {\n\t\tcommon.ErrorResp(c, errs.PermissionDenied, 403)\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\tif !common.HasPermission(perm, common.PermAddOfflineDownload) {\n\t\tcommon.ErrorStrResp(c, \"permission denied\", 403)\n\t\treturn\n\t}\n\tvar tasks []task.TaskExtensionInfo\n\tfor _, url := range req.Urls {\n\t\tt, err := tool.AddURL(c, &tool.AddURLArgs{\n\t\t\tURL:          url,\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/role.go",
    "content": "package handles\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc ListRoles(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\troles, total, err := op.GetRoles(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{Content: roles, Total: total})\n}\n\nfunc GetRole(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\trole, err := op.GetRole(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c, role)\n}\n\nfunc CreateRole(c *gin.Context) {\n\tvar req model.Role\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := op.CreateRole(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc UpdateRole(c *gin.Context) {\n\tvar req struct {\n\t\tID               uint                    `json:\"id\"`\n\t\tName             string                  `json:\"name\" binding:\"required\"`\n\t\tDescription      string                  `json:\"description\"`\n\t\tPermissionScopes []model.PermissionEntry `json:\"permission_scopes\"`\n\t\tDefault          *bool                   `json:\"default\"`\n\t}\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\trole, err := op.GetRole(req.ID)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tswitch role.Name {\n\tcase \"admin\":\n\t\tcommon.ErrorResp(c, errs.ErrChangeDefaultRole, 403)\n\t\treturn\n\n\tcase \"guest\":\n\t\treq.Name = \"guest\"\n\t}\n\trole.Name = req.Name\n\trole.Description = req.Description\n\trole.PermissionScopes = req.PermissionScopes\n\tif req.Default != nil {\n\t\trole.Default = *req.Default\n\t}\n\tif err := op.UpdateRole(role); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t} else {\n\t\tcommon.SuccessResp(c)\n\t}\n}\n\nfunc DeleteRole(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\trole, err := op.GetRole(uint(id))\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tif role.Name == \"admin\" || role.Name == \"guest\" {\n\t\tcommon.ErrorResp(c, errs.ErrChangeDefaultRole, 403)\n\t\treturn\n\t}\n\tif err := op.DeleteRole(uint(id)); err != nil {\n\t\tcommon.ErrorResp(c, err, 500, true)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/search.go",
    "content": "package handles\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/search\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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.MustGet(\"user\").(*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\tvar (\n\t\tfilteredNodes []model.SearchNode\n\t)\n\tfor len(filteredNodes) < req.PerPage {\n\t\tnodes, _, err := search.Search(c, req.SearchReq)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tif len(nodes) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfor _, node := range nodes {\n\t\t\tif !strings.HasPrefix(node.Parent, user.BasePath) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmeta, err := op.GetNearestMeta(node.Parent)\n\t\t\tif err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfilteredNodes = append(filteredNodes, node)\n\t\t\tif len(filteredNodes) >= req.PerPage {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treq.Page++\n\t}\n\tcommon.SuccessResp(c, common.PageResp{\n\t\tContent: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),\n\t\tTotal:   int64(len(filteredNodes)),\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/session.go",
    "content": "package handles\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype SessionResp struct {\n\tSessionID  string `json:\"session_id\"`\n\tUserID     uint   `json:\"user_id,omitempty\"`\n\tLastActive int64  `json:\"last_active\"`\n\tStatus     int    `json:\"status\"`\n\tUA         string `json:\"ua\"`\n\tIP         string `json:\"ip\"`\n}\n\nfunc ListMySessions(c *gin.Context) {\n\tuser := c.MustGet(\"user\").(*model.User)\n\tsessions, err := db.ListSessionsByUser(user.ID)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tresp := make([]SessionResp, len(sessions))\n\tfor i, s := range sessions {\n\t\tresp[i] = SessionResp{\n\t\t\tSessionID:  s.DeviceKey,\n\t\t\tLastActive: s.LastActive,\n\t\t\tStatus:     s.Status,\n\t\t\tUA:         s.UserAgent,\n\t\t\tIP:         s.IP,\n\t\t}\n\t}\n\tcommon.SuccessResp(c, resp)\n}\n\ntype EvictSessionReq struct {\n\tSessionID string `json:\"session_id\"`\n}\n\nfunc EvictMySession(c *gin.Context) {\n\tvar req EvictSessionReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tuser := c.MustGet(\"user\").(*model.User)\n\tif _, err := db.GetSession(user.ID, req.SessionID); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := db.MarkInactive(req.SessionID); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n\nfunc ListSessions(c *gin.Context) {\n\tsessions, err := db.ListSessions()\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tresp := make([]SessionResp, len(sessions))\n\tfor i, s := range sessions {\n\t\tresp[i] = SessionResp{\n\t\t\tSessionID:  s.DeviceKey,\n\t\t\tUserID:     s.UserID,\n\t\t\tLastActive: s.LastActive,\n\t\t\tStatus:     s.Status,\n\t\t\tUA:         s.UserAgent,\n\t\t\tIP:         s.IP,\n\t\t}\n\t}\n\tcommon.SuccessResp(c, resp)\n}\n\nfunc EvictSession(c *gin.Context) {\n\tvar req EvictSessionReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\tif err := db.MarkInactive(req.SessionID); err != nil {\n\t\tcommon.ErrorResp(c, err, 500)\n\t\treturn\n\t}\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/setting.go",
    "content": "package handles\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/server/static\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc getRoleOptions() string {\n\troles, _, err := op.GetRoles(1, model.MaxInt)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tnames := make([]string, 0, len(roles))\n\tfor _, r := range roles {\n\t\tif r.Name == \"admin\" || r.Name == \"guest\" {\n\t\t\tcontinue\n\t\t}\n\t\tnames = append(names, r.Name)\n\t}\n\treturn strings.Join(names, \",\")\n}\n\ntype SetTokenReq struct {\n\tToken string `json:\"token\" form:\"token\" binding:\"required\"`\n}\n\nfunc ResetToken(c *gin.Context) {\n\ttoken := random.Token()\n\titem := model.SettingItem{Key: conf.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 SetToken(c *gin.Context) {\n\tvar req SetTokenReq\n\tif err := c.ShouldBind(&req); err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t\treturn\n\t}\n\titem := model.SettingItem{Key: conf.Token, Value: req.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, req.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\tif item.Key == conf.DefaultRole {\n\t\t\tcopy := *item\n\t\t\tcopy.Options = getRoleOptions()\n\t\t\tif id, err := strconv.Atoi(copy.Value); err == nil {\n\t\t\t\tif r, err := op.GetRole(uint(id)); err == nil {\n\t\t\t\t\tcopy.Value = r.Name\n\t\t\t\t}\n\t\t\t}\n\t\t\tcommon.SuccessResp(c, copy)\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\tfor i := range items {\n\t\t\tif items[i].Key == conf.DefaultRole {\n\t\t\t\tif id, err := strconv.Atoi(items[i].Value); err == nil {\n\t\t\t\t\tif r, err := op.GetRole(uint(id)); err == nil {\n\t\t\t\t\t\titems[i].Value = r.Name\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\titems[i].Options = getRoleOptions()\n\t\t\t\tbreak\n\t\t\t}\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\n\tfor i := range req {\n\t\tif req[i].Key == conf.DefaultRole {\n\t\t\trole, err := op.GetRoleByName(req[i].Value)\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\tif role.Name == \"admin\" || role.Name == \"guest\" {\n\t\t\t\tcommon.ErrorStrResp(c, \"cannot set admin or guest as default role\", 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\treq[i].Value = strconv.Itoa(int(role.ID))\n\t\t}\n\t}\n\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\tfor i := range settings {\n\t\tif settings[i].Key == conf.DefaultRole {\n\t\t\tif id, err := strconv.Atoi(settings[i].Value); err == nil {\n\t\t\t\tif r, err := op.GetRole(uint(id)); err == nil {\n\t\t\t\t\tsettings[i].Value = r.Name\n\t\t\t\t}\n\t\t\t}\n\t\t\tsettings[i].Options = getRoleOptions()\n\t\t\tbreak\n\t\t}\n\t}\n\tcommon.SuccessResp(c, settings)\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/sshkey.go",
    "content": "package handles\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"strconv\"\n\t\"strings\"\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.Value(\"user\").(*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.Value(\"user\").(*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.Value(\"user\").(*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\"github.com/alist-org/alist/v3/internal/op\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Xhofe/go-cache\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/pkg/utils/random\"\n\t\"github.com/alist-org/alist/v3/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.Request) + \"/api/auth/\" + method\n\t} else {\n\t\treturn common.GetApiUrl(c.Request) + \"/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:       model.Roles{op.GetDefaultRoleID()},\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.Request)+\"/@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}\n\t\tif useCompatibility {\n\t\t\tc.Redirect(302, common.GetApiUrl(c.Request)+\"/@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.Request) + \"/api/auth/\" + argument\n\t\t} else {\n\t\t\tredirect_uri = common.GetApiUrl(c.Request) + \"/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.Request)+\"/@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}\n\tif usecompatibility {\n\t\tc.Redirect(302, common.GetApiUrl(c.Request)+\"/@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\"strconv\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\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: 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, 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, 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, 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, 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, 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.StoragesLoaded = false\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.StoragesLoaded = true\n\t}(storages)\n\tcommon.SuccessResp(c)\n}\n"
  },
  {
    "path": "server/handles/task.go",
    "content": "package handles\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/task\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/offline_download/tool\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/xhofe/tache\"\n)\n\ntype TaskInfo struct {\n\tID          string      `json:\"id\"`\n\tName        string      `json:\"name\"`\n\tCreator     string      `json:\"creator\"`\n\tCreatorRole model.Roles `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\tvar creatorRole model.Roles\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.Value(\"user\").(*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(\"/offline_download\"), tool.DownloadTaskManager)\n\ttaskRoute(g.Group(\"/offline_download_transfer\"), tool.TransferTaskManager)\n\ttaskRoute(g.Group(\"/s3_transition\"), fs.S3TransitionTaskManager)\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/alist-org/alist/v3/pkg/utils\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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 len(req.Role) == 0 {\n\t\treq.Role = model.Roles{op.GetDefaultRoleID()}\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\n\tif user.Username == \"admin\" {\n\t\tif !utils.SliceEqual(user.Role, req.Role) {\n\t\t\tcommon.ErrorStrResp(c, \"cannot change role of admin user\", 403)\n\t\t\treturn\n\t\t}\n\t\t//if user.Username != req.Username {\n\t\t//\tcommon.ErrorStrResp(c, \"cannot change username of admin user\", 403)\n\t\t//\treturn\n\t\t//}\n\t}\n\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 && user.IsAdmin() {\n\t\tcount, err := op.CountEnabledAdminsExcluding(user.ID)\n\t\tif err != nil {\n\t\t\tcommon.ErrorResp(c, err, 500)\n\t\t\treturn\n\t\t}\n\t\tif count == 0 {\n\t\t\tcommon.ErrorStrResp(c, \"at least one enabled admin must be kept\", 400)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif !utils.SliceEqual(user.Role, req.Role) {\n\t\tif req.IsAdmin() || req.IsGuest() {\n\t\t\tcommon.ErrorStrResp(c, \"cannot assign admin or guest role to user\", 400, true)\n\t\t\treturn\n\t\t}\n\t}\n\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/alist-org/alist/v3/internal/authn\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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.Request)\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.Request)\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.MustGet(\"user\").(*model.User)\n\n\tauthnInstance, err := authn.NewAuthnInstance(c.Request)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t}\n\n\toptions, sessionData, err := authnInstance.BeginRegistration(user)\n\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\n\t}\n\n\tval, err := json.Marshal(sessionData)\n\tif err != nil {\n\t\tcommon.ErrorResp(c, err, 400)\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.MustGet(\"user\").(*model.User)\n\tsessionDataString := c.GetHeader(\"Session\")\n\n\tauthnInstance, err := authn.NewAuthnInstance(c.Request)\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.MustGet(\"user\").(*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.MustGet(\"user\").(*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\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/device\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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(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\tif !HandleSession(c, admin) {\n\t\t\treturn\n\t\t}\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\tif guest.Disabled {\n\t\t\tcommon.ErrorStrResp(c, \"Guest user is disabled, login please\", 401)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tif len(guest.Role) > 0 {\n\t\t\troles, err := op.GetRolesByUserID(guest.ID)\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"Fail to load guest roles: %v\", err), 500)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tguest.RolesDetail = roles\n\t\t}\n\t\tif !HandleSession(c, guest) {\n\t\t\treturn\n\t\t}\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\tif len(user.Role) > 0 {\n\t\troles, err := op.GetRolesByUserID(user.ID)\n\t\tif err != nil {\n\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"Fail to load roles: %v\", err), 500)\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tuser.RolesDetail = roles\n\t}\n\tif !HandleSession(c, user) {\n\t\treturn\n\t}\n\tlog.Debugf(\"use login token: %+v\", user)\n\tc.Next()\n}\n\n// HandleSession verifies device sessions and stores context values.\nfunc HandleSession(c *gin.Context, user *model.User) bool {\n\tclientID := c.GetHeader(\"Client-Id\")\n\tif clientID == \"\" {\n\t\tclientID = c.Query(\"client_id\")\n\t}\n\tkey := utils.GetMD5EncodeStr(fmt.Sprintf(\"%d-%s\", user.ID, clientID))\n\tif err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {\n\t\ttoken := c.GetHeader(\"Authorization\")\n\t\tif errors.Is(err, errs.SessionInactive) {\n\t\t\t_ = common.InvalidateToken(token)\n\t\t\tcommon.ErrorResp(c, err, 401)\n\t\t} else {\n\t\t\tcommon.ErrorResp(c, err, 403)\n\t\t}\n\t\tc.Abort()\n\t\treturn false\n\t}\n\tc.Set(\"device_key\", key)\n\tc.Set(\"user\", user)\n\treturn true\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\tc.Set(\"user\", 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\tc.Set(\"user\", 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\tif len(user.Role) > 0 {\n\t\tvar roles []model.Role\n\t\tfor _, roleID := range user.Role {\n\t\t\trole, err := op.GetRole(uint(roleID))\n\t\t\tif err != nil {\n\t\t\t\tcommon.ErrorStrResp(c, fmt.Sprintf(\"load role %d failed\", roleID), 500)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\troles = append(roles, *role)\n\t\t}\n\t\tuser.RolesDetail = roles\n\t}\n\tc.Set(\"user\", user)\n\tlog.Debugf(\"use login token: %+v\", user)\n\tc.Next()\n}\n\nfunc AuthNotGuest(c *gin.Context) {\n\tuser := c.MustGet(\"user\").(*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.MustGet(\"user\").(*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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc StoragesLoaded(c *gin.Context) {\n\tif conf.StoragesLoaded {\n\t\tc.Next()\n\t} else {\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\tcommon.ErrorStrResp(c, \"Loading storage, please wait\", 500)\n\t\tc.Abort()\n\t}\n}\n"
  },
  {
    "path": "server/middlewares/down.go",
    "content": "package middlewares\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc Down(verifyFunc func(string, string) error) func(c *gin.Context) {\n\treturn func(c *gin.Context) {\n\t\trawPath := parsePath(c.Param(\"path\"))\n\t\tc.Set(\"path\", rawPath)\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.ErrorResp(c, err, 500, true)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tc.Set(\"meta\", 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.ErrorResp(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\nfunc parsePath(path string) string {\n\tpath, _ = url.PathUnescape(path)\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/fsup.go",
    "content": "package middlewares\n\nimport (\n\t\"net/url\"\n\tstdpath \"path\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/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.MustGet(\"user\").(*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\tperm := common.MergeRolePermissions(user, path)\n\tif !(common.CanAccessWithRoles(user, meta, path, password) &&\n\t\t(common.HasPermission(perm, common.PermWrite) || 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/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/gin-gonic/gin\"\n\t\"io\"\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/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, 500)\n\t\tc.Abort()\n\t} else {\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "server/middlewares/session_refresh.go",
    "content": "package middlewares\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/device\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// SessionRefresh updates session's last_active after successful requests.\nfunc SessionRefresh(c *gin.Context) {\n\tc.Next()\n\tif c.Writer.Status() >= 400 {\n\t\treturn\n\t}\n\tif inactive, ok := c.Get(\"session_inactive\"); ok {\n\t\tif b, ok := inactive.(bool); ok && b {\n\t\t\treturn\n\t\t}\n\t}\n\tuserVal, uok := c.Get(\"user\")\n\tkeyVal, kok := c.Get(\"device_key\")\n\tif uok && kok {\n\t\tuser := userVal.(*model.User)\n\t\tdevice.Refresh(user.ID, keyVal.(string))\n\t}\n}\n"
  },
  {
    "path": "server/router.go",
    "content": "package server\n\nimport (\n\t\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/message\"\n\t\"github.com/alist-org/alist/v3/internal/sign\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/server/handles\"\n\t\"github.com/alist-org/alist/v3/server/middlewares\"\n\t\"github.com/alist-org/alist/v3/server/static\"\n\t\"github.com/gin-contrib/cors\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Init(e *gin.Engine) {\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\te.Use(middlewares.SessionRefresh)\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(\"/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\", signCheck, downloadLimiter, handles.Down)\n\tg.GET(\"/p/*path\", signCheck, downloadLimiter, handles.Proxy)\n\tg.HEAD(\"/d/*path\", signCheck, handles.Down)\n\tg.HEAD(\"/p/*path\", signCheck, handles.Proxy)\n\tarchiveSignCheck := middlewares.Down(sign.VerifyArchive)\n\tg.GET(\"/ad/*path\", archiveSignCheck, downloadLimiter, handles.ArchiveDown)\n\tg.GET(\"/ap/*path\", archiveSignCheck, downloadLimiter, handles.ArchiveProxy)\n\tg.GET(\"/ae/*path\", archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)\n\tg.HEAD(\"/ad/*path\", archiveSignCheck, handles.ArchiveDown)\n\tg.HEAD(\"/ap/*path\", archiveSignCheck, handles.ArchiveProxy)\n\tg.HEAD(\"/ae/*path\", archiveSignCheck, handles.ArchiveInternalExtract)\n\n\tapi := g.Group(\"/api\")\n\tauth := api.Group(\"\", middlewares.Auth)\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\tapi.POST(\"/auth/register\", handles.Register)\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\tauth.GET(\"/me/sessions\", handles.ListMySessions)\n\tauth.POST(\"/me/sessions/evict\", handles.EvictMySession)\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\t_task(auth.Group(\"/task\", middlewares.AuthNotGuest))\n\t_label(auth.Group(\"/label\"))\n\t_labelFileBinding(auth.Group(\"/label_file_binding\"))\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\trole := g.Group(\"/role\")\n\trole.GET(\"/list\", handles.ListRoles)\n\trole.GET(\"/get\", handles.GetRole)\n\trole.POST(\"/create\", handles.CreateRole)\n\trole.POST(\"/update\", handles.UpdateRole)\n\trole.POST(\"/delete\", handles.DeleteRole)\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(\"/reset_token\", handles.ResetToken)\n\tsetting.POST(\"/set_token\", handles.SetToken)\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_pikpak\", handles.SetPikPak)\n\tsetting.POST(\"/set_thunder\", handles.SetThunder)\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\tlabel := g.Group(\"/label\")\n\tlabel.POST(\"/create\", handles.CreateLabel)\n\tlabel.POST(\"/update\", handles.UpdateLabel)\n\tlabel.POST(\"/delete\", handles.DeleteLabel)\n\n\tlabelFileBinding := g.Group(\"/label_file_binding\")\n\tlabelFileBinding.GET(\"/list\", handles.ListLabelFileBinding)\n\tlabelFileBinding.POST(\"/create\", handles.CreateLabelFileBinDing)\n\tlabelFileBinding.POST(\"/create_batch\", handles.CreateLabelFileBinDingBatch)\n\tlabelFileBinding.POST(\"/delete\", handles.DelLabelByFileName)\n\tlabelFileBinding.POST(\"/restore\", handles.RestoreLabelFileBinding)\n\n\tsession := g.Group(\"/session\")\n\tsession.GET(\"/list\", handles.ListSessions)\n\tsession.POST(\"/evict\", handles.EvictSession)\n\n}\n\nfunc _fs(g *gin.RouterGroup) {\n\tg.Any(\"/list\", handles.FsList)\n\tg.Any(\"/search\", middlewares.SearchIndex, handles.Search)\n\tg.Any(\"/get\", handles.FsGet)\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\ta := g.Group(\"/archive\")\n\ta.Any(\"/meta\", handles.FsArchiveMeta)\n\ta.Any(\"/list\", handles.FsArchiveList)\n\ta.POST(\"/decompress\", handles.FsArchiveDecompress)\n}\n\nfunc _task(g *gin.RouterGroup) {\n\thandles.SetupTaskRoute(g)\n}\n\nfunc _label(g *gin.RouterGroup) {\n\tg.GET(\"/list\", handles.ListLabel)\n\tg.GET(\"/get\", handles.GetLabel)\n}\n\nfunc _labelFileBinding(g *gin.RouterGroup) {\n\tg.GET(\"/get\", handles.GetLabelByFileName)\n\tg.GET(\"/get_file_by_label\", handles.GetFileByLabel)\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 alist\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/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/pkg/http_range\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/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, \"meta\", 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) (obj *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, \"meta\", 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\n\tsize := file.GetSize()\n\trnge, err := rangeRequest.Range(size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif link.RangeReadCloser == nil && link.MFile == nil && len(link.URL) == 0 {\n\t\treturn nil, fmt.Errorf(\"the remote storage driver need to be enhanced to support s3\")\n\t}\n\n\tvar rdr io.ReadCloser\n\tlength := int64(-1)\n\tstart := int64(0)\n\tif rnge != nil {\n\t\tstart, length = rnge.Start, rnge.Length\n\t}\n\t// 参考 server/common/proxy.go\n\tif link.MFile != nil {\n\t\t_, err := link.MFile.Seek(start, io.SeekStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trdr = link.MFile\n\t} else {\n\t\tremoteFileSize := file.GetSize()\n\t\tif length >= 0 && start+length >= remoteFileSize {\n\t\t\tlength = -1\n\t\t}\n\t\trrc := link.RangeReadCloser\n\t\tif len(link.URL) > 0 {\n\t\t\tvar converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, link)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trrc = converted\n\t\t}\n\t\tif rrc != nil {\n\t\t\tremoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: start, Length: length})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trdr = utils.ReadCloser{Reader: remoteReader, Closer: rrc}\n\t\t} else {\n\t\t\treturn nil, errs.NotSupport\n\t\t}\n\t}\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\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: rdr,\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, \"meta\", 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, true)\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\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\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\tif 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\tutils.Log.Errorf(\"serve s3\", \"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, \"meta\", 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, \"meta\", 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 alist\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 alist\npackage s3\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/gofakes3\"\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\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 alist\npackage s3\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/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 alist\npackage s3\n\nimport (\n\t\"sort\"\n\n\t\"github.com/alist-org/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 alist\npackage s3\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"net/http\"\n\n\t\"github.com/alist-org/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 alist\npackage s3\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/gofakes3\"\n)\n\ntype Bucket struct {\n\tName string `json:\"name\"`\n\tPath string `json:\"path\"`\n}\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, \"meta\", 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, \"meta\", 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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/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\"github.com/alist-org/alist/v3/cmd/flags\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"os\"\n\t\"path/filepath\"\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.Fatalf(\"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.Fatalf(\"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.Fatalf(\"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.Fatalf(\"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.Fatalf(\"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.Fatalf(\"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\"github.com/KirCute/sftpd-alist\"\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/ftp\"\n\t\"os\"\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\"github.com/KirCute/sftpd-alist\"\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/server/ftp\"\n\t\"github.com/alist-org/alist/v3/server/sftp\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype SftpDriver struct {\n\tproxyHeader *http.Header\n\tconfig      *sftpd.Config\n}\n\nfunc NewSftpDriver() (*SftpDriver, error) {\n\tsftp.InitHostKey()\n\theader := &http.Header{}\n\theader.Add(\"User-Agent\", setting.GetStr(conf.FTPProxyUserAgent))\n\treturn &SftpDriver{\n\t\tproxyHeader: header,\n\t}, nil\n}\n\nfunc (d *SftpDriver) GetConfig() *sftpd.Config {\n\tif d.config != nil {\n\t\treturn d.config\n\t}\n\tserverConfig := ssh.ServerConfig{\n\t\tNoClientAuth:         true,\n\t\tNoClientAuthCallback: d.NoClientAuth,\n\t\tPasswordCallback:     d.PasswordAuth,\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, \"user\", userObj)\n\tctx = context.WithValue(ctx, \"meta_pass\", \"\")\n\tctx = context.WithValue(ctx, \"client_ip\", sc.RemoteAddr().String())\n\tctx = context.WithValue(ctx, \"proxy_header\", 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\tpermGuest := common.MergeRolePermissions(guest, guest.BasePath)\n\tif guest.Disabled || !common.HasPermission(permGuest, common.PermFTPAccess) {\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\tuserObj, err := op.GetUserByName(conn.User())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tperm := common.MergeRolePermissions(userObj, userObj.BasePath)\n\tif userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {\n\t\treturn nil, errors.New(\"user is not allowed to access via SFTP\")\n\t}\n\tpassHash := model.StaticHash(string(password))\n\tif err = userObj.ValidatePwdStaticHash(passHash); err != nil {\n\t\treturn nil, err\n\t}\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\tperm := common.MergeRolePermissions(userObj, userObj.BasePath)\n\tif userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {\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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/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\", conf.WebVersion),\n\t}\n\tif siteConfig.BasePath != \"\" {\n\t\tsiteConfig.BasePath = utils.FixAndCleanPath(siteConfig.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\"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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/public\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar static fs.FS\n\nfunc initStatic() {\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\")\n\t\t}\n\t\tstatic = dist\n\t\treturn\n\t}\n\tstatic = os.DirFS(conf.Conf.DistDir)\n}\n\nfunc initIndex() {\n\tindexFile, err := static.Open(\"index.html\")\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tutils.Log.Fatalf(\"index.html not exist, you may forget to put dist of frontend to public/dist\")\n\t\t}\n\t\tutils.Log.Fatalf(\"failed to read index.html: %v\", err)\n\t}\n\tdefer func() {\n\t\t_ = indexFile.Close()\n\t}()\n\tindex, err := io.ReadAll(indexFile)\n\tif err != nil {\n\t\tutils.Log.Fatalf(\"failed to read dist/index.html\")\n\t}\n\tconf.RawIndexHtml = string(index)\n\tsiteConfig := getSiteConfig()\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}\n\tfor k, v := range replaceMap {\n\t\tconf.RawIndexHtml = strings.Replace(conf.RawIndexHtml, k, v, 1)\n\t}\n\tUpdateIndex()\n}\n\nfunc UpdateIndex() {\n\tfavicon := setting.GetStr(conf.Favicon)\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\tconf.ManageHtml = conf.RawIndexHtml\n\treplaceMap1 := map[string]string{\n\t\t\"https://jsd.nn.ci/gh/alist-org/logo@main/logo.svg\": favicon,\n\t\t\"Loading...\":            title,\n\t\t\"main_color: undefined\": fmt.Sprintf(\"main_color: '%s'\", mainColor),\n\t}\n\tfor k, v := range replaceMap1 {\n\t\tconf.ManageHtml = strings.Replace(conf.ManageHtml, k, v, 1)\n\t}\n\tconf.IndexHtml = conf.ManageHtml\n\treplaceMap2 := map[string]string{\n\t\t\"<!-- customize head -->\": customizeHead,\n\t\t\"<!-- customize body -->\": customizeBody,\n\t}\n\tfor k, v := range replaceMap2 {\n\t\tconf.IndexHtml = strings.Replace(conf.IndexHtml, k, v, 1)\n\t}\n}\n\nfunc Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {\n\tinitStatic()\n\tinitIndex()\n\tfolders := []string{\"assets\", \"images\", \"streamer\", \"static\"}\n\tr.Use(func(c *gin.Context) {\n\t\tfor i := range folders {\n\t\t\tif strings.HasPrefix(c.Request.RequestURI, fmt.Sprintf(\"/%s/\", folders[i])) {\n\t\t\t\tc.Header(\"Cache-Control\", \"public, max-age=15552000\")\n\t\t\t}\n\t\t}\n\t})\n\tfor i, folder := range folders {\n\t\tsub, err := fs.Sub(static, folder)\n\t\tif err != nil {\n\t\t\tutils.Log.Fatalf(\"can't find folder: %s\", folder)\n\t\t}\n\t\tr.StaticFS(fmt.Sprintf(\"/%s/\", folders[i]), http.FS(sub))\n\t}\n\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/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/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/server/common\"\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(\"user\").(*model.User)\n\tperm := common.MergeRolePermissions(user, src)\n\tif srcDir != dstDir && !common.HasPermission(perm, common.PermMove) {\n\t\treturn http.StatusForbidden, nil\n\t}\n\tif srcName != dstName && !common.HasPermission(perm, common.PermRename) {\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\terr = fs.Move(ctx, 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\tuser := ctx.Value(\"user\").(*model.User)\n\t// Read directory names.\n\tobjs, err := fs.List(context.WithValue(ctx, \"meta\", 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 !common.CanReadPathByRole(user, filename) {\n\t\t\tcontinue\n\t\t}\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/path.go",
    "content": "package webdav\n\nimport (\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n)\n\n// ResolvePath normalizes the provided raw path and resolves it against the user's base path\n// before delegating to the user-aware JoinPath permission checks.\nfunc ResolvePath(user *model.User, raw string) (string, error) {\n\tcleaned := utils.FixAndCleanPath(raw)\n\tbasePath := utils.FixAndCleanPath(user.BasePath)\n\n\tif cleaned != \"/\" && basePath != \"/\" && !utils.IsSubPath(basePath, cleaned) {\n\t\tcleaned = path.Join(basePath, strings.TrimPrefix(cleaned, \"/\"))\n\t}\n\n\treturn user.JoinPath(cleaned)\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\"mime\"\n\t\"net/http\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/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(\"userAgent\").(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 := mime.TypeByExtension(path.Ext(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\t// The Apache http 2.4 web server by default concatenates the\n\t// modification time and size of a file. We replicate the heuristic\n\t// with nanosecond granularity.\n\treturn common.GetEtag(fi), 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\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/fs\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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 r.Method == http.MethodHead {\n\t\tw.Header().Set(\"Content-Length\", fmt.Sprintf(\"%d\", fi.GetSize()))\n\t\treturn http.StatusOK, nil\n\t}\n\tif fi.IsDir() {\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\tdownProxyUrl := storage.GetStorage().DownProxyUrl\n\tif storage.GetStorage().WebdavNative() || (storage.GetStorage().WebdavProxy() && downProxyUrl == \"\") {\n\t\tlink, _, err := fs.Link(ctx, reqPath, model.LinkArgs{Header: r.Header, HttpReq: r})\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\tif storage.GetStorage().ProxyRange {\n\t\t\tcommon.ProxyRange(link, fi.GetSize())\n\t\t}\n\t\terr = common.Proxy(w, r, link, fi)\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, fmt.Errorf(\"webdav proxy error: %+v\", err)\n\t\t}\n\t} else if storage.GetStorage().WebdavProxy() && downProxyUrl != \"\" {\n\t\tu := common.BuildDownProxyURL(downProxyUrl, reqPath, storage.GetStorage().DownProxySign)\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=0, no-cache, no-store, must-revalidate\")\n\t\thttp.Redirect(w, r, u, http.StatusFound)\n\t} else {\n\t\tlink, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, HttpReq: r, Redirect: true})\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\thttp.Redirect(w, r, link.URL, http.StatusFound)\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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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\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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, reqPath)\n\tif err != nil {\n\t\treturn http.StatusForbidden, err\n\t}\n\tobj := model.Object{\n\t\tName:     path.Base(reqPath),\n\t\tSize:     r.ContentLength,\n\t\tModified: h.getModTime(r),\n\t\tCtime:    h.getCreateTime(r),\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_ = r.Body.Close()\n\t_ = fsStream.Close()\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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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(\"user\").(*model.User)\n\tsrc, err = ResolvePath(user, src)\n\tif err != nil {\n\t\treturn 403, err\n\t}\n\tdst, err = ResolvePath(user, 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(\"user\").(*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 = ResolvePath(user, 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, \"userAgent\", userAgent)\n\tuser := ctx.Value(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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\tif utils.PathEqual(reqPath, user.BasePath) {\n\t\thasRootPerm := false\n\t\tfor _, role := range user.RolesDetail {\n\t\t\tfor _, entry := range role.PermissionScopes {\n\t\t\t\tif utils.PathEqual(entry.Path, user.BasePath) {\n\t\t\t\t\thasRootPerm = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif hasRootPerm {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !hasRootPerm {\n\t\t\tbasePaths := model.GetAllBasePathsFromRoles(user)\n\t\t\ttype infoItem struct {\n\t\t\t\tpath string\n\t\t\t\tinfo model.Obj\n\t\t\t}\n\t\t\tinfos := []infoItem{{reqPath, fi}}\n\t\t\tseen := make(map[string]struct{})\n\t\t\tfor _, p := range basePaths {\n\t\t\t\tif !utils.IsSubPath(user.BasePath, p) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\trel := strings.TrimPrefix(\n\t\t\t\t\tstrings.TrimPrefix(\n\t\t\t\t\t\tutils.FixAndCleanPath(p),\n\t\t\t\t\t\tutils.FixAndCleanPath(user.BasePath),\n\t\t\t\t\t),\n\t\t\t\t\t\"/\",\n\t\t\t\t)\n\t\t\t\tdir := strings.Split(rel, \"/\")[0]\n\t\t\t\tif dir == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif _, ok := seen[dir]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tseen[dir] = struct{}{}\n\t\t\t\tsp := utils.FixAndCleanPath(path.Join(user.BasePath, dir))\n\t\t\t\tinfo, err := fs.Get(ctx, sp, &fs.GetArgs{})\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tinfos = append(infos, infoItem{sp, info})\n\t\t\t}\n\t\t\tfor _, item := range infos {\n\t\t\t\tvar pstats []Propstat\n\t\t\t\tif pf.Propname != nil {\n\t\t\t\t\tpnames, err := propnames(ctx, h.LockSystem, item.info)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn http.StatusInternalServerError, err\n\t\t\t\t\t}\n\t\t\t\t\tpstat := Propstat{Status: http.StatusOK}\n\t\t\t\t\tfor _, xmlname := range pnames {\n\t\t\t\t\t\tpstat.Props = append(pstat.Props, Property{XMLName: xmlname})\n\t\t\t\t\t}\n\t\t\t\t\tpstats = append(pstats, pstat)\n\t\t\t\t} else if pf.Allprop != nil {\n\t\t\t\t\tpstats, err = allprop(ctx, h.LockSystem, item.info, pf.Prop)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn http.StatusInternalServerError, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tpstats, err = props(ctx, h.LockSystem, item.info, pf.Prop)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn http.StatusInternalServerError, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trel := strings.TrimPrefix(\n\t\t\t\t\tstrings.TrimPrefix(\n\t\t\t\t\t\tutils.FixAndCleanPath(item.path),\n\t\t\t\t\t\tutils.FixAndCleanPath(user.BasePath),\n\t\t\t\t\t),\n\t\t\t\t\t\"/\",\n\t\t\t\t)\n\t\t\t\thref := utils.EncodePath(path.Join(\"/\", h.Prefix, rel), true)\n\t\t\t\tif href != \"/\" && item.info.IsDir() {\n\t\t\t\t\thref += \"/\"\n\t\t\t\t}\n\t\t\t\tif err := mw.write(makePropstatResponse(href, pstats)); err != nil {\n\t\t\t\t\treturn http.StatusInternalServerError, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := mw.close(); err != nil {\n\t\t\t\treturn http.StatusInternalServerError, err\n\t\t\t}\n\t\t\treturn 0, nil\n\t\t}\n\t}\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\trel := strings.TrimPrefix(\n\t\t\tstrings.TrimPrefix(\n\t\t\t\tutils.FixAndCleanPath(reqPath),\n\t\t\t\tutils.FixAndCleanPath(user.BasePath),\n\t\t\t),\n\t\t\t\"/\",\n\t\t)\n\t\thref := utils.EncodePath(path.Join(\"/\", h.Prefix, rel), true)\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(\"user\").(*model.User)\n\treqPath, err = ResolvePath(user, 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{href},\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/alist-org/alist/v3/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/alist-org/alist/v3/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\"context\"\n\t\"crypto/subtle\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/alist-org/alist/v3/internal/errs\"\n\t\"github.com/alist-org/alist/v3/internal/stream\"\n\t\"github.com/alist-org/alist/v3/server/middlewares\"\n\n\t\"github.com/alist-org/alist/v3/internal/conf\"\n\t\"github.com/alist-org/alist/v3/internal/device\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/alist-org/alist/v3/internal/op\"\n\t\"github.com/alist-org/alist/v3/internal/setting\"\n\t\"github.com/alist-org/alist/v3/pkg/utils\"\n\t\"github.com/alist-org/alist/v3/server/common\"\n\t\"github.com/alist-org/alist/v3/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\t// Skip logging for NotFoundError as it's not a program error\n\t\t\t// but a normal case when a file doesn't exist\n\t\t\tif errs.IsNotFoundError(err) {\n\t\t\t\tlog.Debugf(\"%s %s %v\", request.Method, request.URL.Path, err)\n\t\t\t\treturn\n\t\t\t}\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\tuser := c.MustGet(\"user\").(*model.User)\n\tctx := context.WithValue(c.Request.Context(), \"user\", user)\n\thandler.ServeHTTP(c.Writer, c.Request.WithContext(ctx))\n}\n\nfunc WebDAVAuth(c *gin.Context) {\n\tguest, _ := op.GetGuest()\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\tkey := utils.GetMD5EncodeStr(fmt.Sprintf(\"%d-%s\", admin.ID, c.ClientIP()))\n\t\t\t\tif err := device.Handle(admin.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {\n\t\t\t\t\tc.Status(http.StatusForbidden)\n\t\t\t\t\tc.Abort()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tc.Set(\"device_key\", key)\n\t\t\t\tc.Set(\"user\", 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\tc.Set(\"user\", guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tc.Writer.Header()[\"WWW-Authenticate\"] = []string{`Basic realm=\"alist\"`}\n\t\tc.Status(http.StatusUnauthorized)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tuser, err := op.GetUserByName(username)\n\tif err != nil || user.ValidateRawPassword(password) != nil {\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tc.Set(\"user\", guest)\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tc.Status(http.StatusUnauthorized)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif roles, err := op.GetRolesByUserID(user.ID); err == nil {\n\t\tuser.RolesDetail = roles\n\t}\n\treqPath := c.Param(\"path\")\n\tif reqPath == \"\" {\n\t\treqPath = \"/\"\n\t}\n\treqPath, _ = url.PathUnescape(reqPath)\n\treqPath, err = webdav.ResolvePath(user, reqPath)\n\tif err != nil {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tperm := common.MergeRolePermissions(user, reqPath)\n\twebdavRead := common.HasPermission(perm, common.PermWebdavRead)\n\tif user.Disabled || (!webdavRead && (c.Request.Method != \"PROPFIND\" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) {\n\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\tc.Set(\"user\", 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\") && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermWrite)) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"MOVE\" && (!common.HasPermission(perm, common.PermWebdavManage) || (!common.HasPermission(perm, common.PermMove) && !common.HasPermission(perm, common.PermRename))) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"COPY\" && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermCopy)) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"DELETE\" && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermRemove)) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tif c.Request.Method == \"PROPPATCH\" && !common.HasPermission(perm, common.PermWebdavManage) {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tkey := utils.GetMD5EncodeStr(fmt.Sprintf(\"%d-%s\", user.ID, c.ClientIP()))\n\tif err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {\n\t\tc.Status(http.StatusForbidden)\n\t\tc.Abort()\n\t\treturn\n\t}\n\tc.Set(\"device_key\", key)\n\tc.Set(\"user\", user)\n\tc.Next()\n}\n"
  },
  {
    "path": "wrapper/zcc-arm64",
    "content": "#!/bin/sh\nzig cc -target aarch64-windows-gnu $@\n"
  },
  {
    "path": "wrapper/zcxx-arm64",
    "content": "#!/bin/sh\nzig c++ -target aarch64-windows-gnu $@\n"
  }
]